tapioca 0.7.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/README.md +491 -73
- data/lib/tapioca/cli.rb +40 -3
- data/lib/tapioca/commands/annotations.rb +154 -0
- data/lib/tapioca/commands/dsl.rb +20 -1
- data/lib/tapioca/commands/gem.rb +17 -57
- data/lib/tapioca/commands/init.rb +1 -0
- data/lib/tapioca/commands.rb +1 -0
- data/lib/tapioca/dsl/compilers/protobuf.rb +14 -0
- data/lib/tapioca/dsl/pipeline.rb +4 -0
- data/lib/tapioca/dsl.rb +6 -0
- data/lib/tapioca/executor.rb +4 -46
- data/lib/tapioca/gem/listeners/methods.rb +26 -1
- data/lib/tapioca/gem/listeners/sorbet_props.rb +1 -1
- data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +1 -0
- data/lib/tapioca/gem/pipeline.rb +4 -0
- data/lib/tapioca/gemfile.rb +50 -3
- data/lib/tapioca/helpers/config_helper.rb +13 -0
- data/lib/tapioca/helpers/rbi_helper.rb +124 -0
- data/lib/tapioca/helpers/shims_helper.rb +36 -8
- data/lib/tapioca/helpers/sorbet_helper.rb +3 -9
- data/lib/tapioca/helpers/test/content.rb +1 -0
- data/lib/tapioca/helpers/test/dsl_compiler.rb +1 -0
- data/lib/tapioca/helpers/test/template.rb +1 -0
- data/lib/tapioca/internal.rb +2 -0
- data/lib/tapioca/rbi_ext/model.rb +2 -0
- data/lib/tapioca/repo_index.rb +41 -0
- data/lib/tapioca/runtime/loader.rb +3 -0
- data/lib/tapioca/runtime/reflection.rb +12 -12
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +33 -46
- data/lib/tapioca/static/symbol_table_parser.rb +2 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +5 -0
- metadata +22 -18
data/lib/tapioca/cli.rb
CHANGED
@@ -5,6 +5,7 @@ module Tapioca
|
|
5
5
|
class Cli < Thor
|
6
6
|
include CliHelper
|
7
7
|
include ConfigHelper
|
8
|
+
include SorbetHelper
|
8
9
|
include ShimsHelper
|
9
10
|
|
10
11
|
FILE_HEADER_OPTION_DESC = "Add a \"This file is generated\" header on top of each generated RBI file"
|
@@ -89,7 +90,7 @@ module Tapioca
|
|
89
90
|
option :quiet,
|
90
91
|
aliases: ["-q"],
|
91
92
|
type: :boolean,
|
92
|
-
desc: "
|
93
|
+
desc: "Suppresses file creation output",
|
93
94
|
default: false
|
94
95
|
option :workers,
|
95
96
|
aliases: ["-w"],
|
@@ -171,7 +172,7 @@ module Tapioca
|
|
171
172
|
option :doc,
|
172
173
|
type: :boolean,
|
173
174
|
desc: "Include YARD documentation from sources when generating RBIs. Warning: this might be slow",
|
174
|
-
default:
|
175
|
+
default: true
|
175
176
|
option :exported_gem_rbis,
|
176
177
|
type: :boolean,
|
177
178
|
desc: "Include RBIs found in the `rbi/` directory of the gem",
|
@@ -242,6 +243,7 @@ module Tapioca
|
|
242
243
|
option :gem_rbi_dir, type: :string, desc: "Path to gem RBIs", default: DEFAULT_GEM_DIR
|
243
244
|
option :dsl_rbi_dir, type: :string, desc: "Path to DSL RBIs", default: DEFAULT_DSL_DIR
|
244
245
|
option :shim_rbi_dir, type: :string, desc: "Path to shim RBIs", default: DEFAULT_SHIM_DIR
|
246
|
+
option :payload, type: :boolean, desc: "Check shims against Sorbet's payload", default: true
|
245
247
|
def check_shims
|
246
248
|
index = RBI::Index.new
|
247
249
|
|
@@ -251,6 +253,30 @@ module Tapioca
|
|
251
253
|
exit(0)
|
252
254
|
end
|
253
255
|
|
256
|
+
payload_path = T.let(nil, T.nilable(String))
|
257
|
+
|
258
|
+
if options[:payload]
|
259
|
+
if sorbet_supports?(:print_payload_sources)
|
260
|
+
Dir.mktmpdir do |dir|
|
261
|
+
payload_path = dir
|
262
|
+
result = sorbet("--no-config --print=payload-sources:#{payload_path}")
|
263
|
+
|
264
|
+
unless result.status
|
265
|
+
say_error("Sorbet failed to dump payload")
|
266
|
+
say_error(result.err)
|
267
|
+
exit(1)
|
268
|
+
end
|
269
|
+
|
270
|
+
index_payload(index, payload_path)
|
271
|
+
end
|
272
|
+
else
|
273
|
+
say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
|
274
|
+
say_error("Current: v#{SORBET_GEM_SPEC.version}")
|
275
|
+
say_error("Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}")
|
276
|
+
exit(1)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
254
280
|
index_rbis(index, "shim", shim_rbi_dir)
|
255
281
|
index_rbis(index, "gem", options[:gem_rbi_dir])
|
256
282
|
index_rbis(index, "dsl", options[:dsl_rbi_dir])
|
@@ -260,7 +286,11 @@ module Tapioca
|
|
260
286
|
duplicates.each do |key, nodes|
|
261
287
|
say_error("\nDuplicated RBI for #{key}:", :red)
|
262
288
|
nodes.each do |node|
|
263
|
-
|
289
|
+
node_loc = node.loc
|
290
|
+
next unless node_loc
|
291
|
+
|
292
|
+
loc_string = location_to_payload_url(node_loc, path_prefix: payload_path)
|
293
|
+
say_error(" * #{loc_string}", :red)
|
264
294
|
end
|
265
295
|
end
|
266
296
|
say_error("\nPlease remove the duplicated definitions from the #{shim_rbi_dir} directory.", :red)
|
@@ -271,6 +301,13 @@ module Tapioca
|
|
271
301
|
exit(0)
|
272
302
|
end
|
273
303
|
|
304
|
+
desc "annotations", "Pull gem annotations from a central RBI repository"
|
305
|
+
option :repo_uri, type: :string, desc: "Repository URI to pull annotations from", default: CENTRAL_REPO_ROOT_URI
|
306
|
+
def annotations
|
307
|
+
command = Commands::Annotations.new(central_repo_root_uri: options[:repo_uri])
|
308
|
+
command.execute
|
309
|
+
end
|
310
|
+
|
274
311
|
map ["--version", "-v"] => :__print_version
|
275
312
|
|
276
313
|
desc "--version, -v", "show version"
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
module Commands
|
8
|
+
class Annotations < Command
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig do
|
12
|
+
params(
|
13
|
+
central_repo_root_uri: String,
|
14
|
+
central_repo_index_path: String
|
15
|
+
).void
|
16
|
+
end
|
17
|
+
def initialize(central_repo_root_uri:, central_repo_index_path: CENTRAL_REPO_INDEX_PATH)
|
18
|
+
super()
|
19
|
+
@central_repo_root_uri = central_repo_root_uri
|
20
|
+
@index = T.let(fetch_index, RepoIndex)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.void }
|
24
|
+
def execute
|
25
|
+
project_gems = list_gemfile_gems
|
26
|
+
remove_expired_annotations(project_gems)
|
27
|
+
fetch_annotations(project_gems)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
sig { returns(T::Array[String]) }
|
33
|
+
def list_gemfile_gems
|
34
|
+
say("Listing gems from Gemfile.lock... ", [:blue, :bold])
|
35
|
+
gemfile = Bundler.read_file("Gemfile.lock")
|
36
|
+
parser = Bundler::LockfileParser.new(gemfile)
|
37
|
+
gem_names = parser.specs.map(&:name)
|
38
|
+
say("Done", :green)
|
39
|
+
gem_names
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(project_gems: T::Array[String]).void }
|
43
|
+
def remove_expired_annotations(project_gems)
|
44
|
+
say("Removing annotations for gems that have been removed... ", [:blue, :bold])
|
45
|
+
|
46
|
+
annotations = Pathname.glob("#{DEFAULT_ANNOTATIONS_DIR}/*.rbi").map { |f| f.basename(".*").to_s }
|
47
|
+
expired = annotations - project_gems
|
48
|
+
|
49
|
+
if expired.empty?
|
50
|
+
say(" Nothing to do")
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
say("\n")
|
55
|
+
expired.each do |gem_name|
|
56
|
+
say("\n")
|
57
|
+
path = "#{DEFAULT_ANNOTATIONS_DIR}/#{gem_name}.rbi"
|
58
|
+
remove_file(path)
|
59
|
+
end
|
60
|
+
say("\nDone\n\n", :green)
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { returns(RepoIndex) }
|
64
|
+
def fetch_index
|
65
|
+
say("Retrieving index from central repository... ", [:blue, :bold])
|
66
|
+
content = fetch_file(CENTRAL_REPO_INDEX_PATH)
|
67
|
+
exit(1) unless content
|
68
|
+
|
69
|
+
index = RepoIndex.from_json(content)
|
70
|
+
say("Done", :green)
|
71
|
+
index
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { params(gem_names: T::Array[String]).returns(T::Array[String]) }
|
75
|
+
def fetch_annotations(gem_names)
|
76
|
+
say("Fetching gem annotations from central repository... ", [:blue, :bold])
|
77
|
+
fetchable_gems = gem_names.select { |gem_name| @index.has_gem?(gem_name) }
|
78
|
+
|
79
|
+
if fetchable_gems.empty?
|
80
|
+
say(" Nothing to do")
|
81
|
+
exit(0)
|
82
|
+
end
|
83
|
+
|
84
|
+
say("\n")
|
85
|
+
fetched_gems = fetchable_gems.select { |name| fetch_annotation(name) }
|
86
|
+
say("\nDone", :green)
|
87
|
+
fetched_gems
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { params(gem_name: String).void }
|
91
|
+
def fetch_annotation(gem_name)
|
92
|
+
content = fetch_file("#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
|
93
|
+
return unless content
|
94
|
+
|
95
|
+
content = add_header(gem_name, content)
|
96
|
+
|
97
|
+
dir = DEFAULT_ANNOTATIONS_DIR
|
98
|
+
FileUtils.mkdir_p(dir)
|
99
|
+
say("\n Fetched #{set_color(gem_name, :yellow, :bold)}", :green)
|
100
|
+
create_file("#{dir}/#{gem_name}.rbi", content)
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
104
|
+
def fetch_file(path)
|
105
|
+
if @central_repo_root_uri.start_with?(%r{https?://})
|
106
|
+
fetch_http_file(path)
|
107
|
+
else
|
108
|
+
fetch_local_file(path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
113
|
+
def fetch_local_file(path)
|
114
|
+
File.read("#{@central_repo_root_uri}/#{path}")
|
115
|
+
rescue => e
|
116
|
+
say_error("\nCan't fetch file `#{path}` (#{e.message})", :bold, :red)
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
121
|
+
def fetch_http_file(path)
|
122
|
+
uri = URI("#{@central_repo_root_uri}/#{path}")
|
123
|
+
response = Net::HTTP.get_response(uri)
|
124
|
+
case response
|
125
|
+
when Net::HTTPSuccess
|
126
|
+
response.body
|
127
|
+
else
|
128
|
+
say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{response.class})", :bold, :red)
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
132
|
+
say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{e.message})", :bold, :red)
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
sig { params(name: String, content: String).returns(String) }
|
137
|
+
def add_header(name, content)
|
138
|
+
header = <<~COMMENT
|
139
|
+
# DO NOT EDIT MANUALLY
|
140
|
+
# This file was pulled from #{@central_repo_root_uri}.
|
141
|
+
# Please run `#{default_command(:annotations)}` to update it.
|
142
|
+
COMMENT
|
143
|
+
|
144
|
+
contents = content.split("\n")
|
145
|
+
if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
|
146
|
+
contents.insert(2, header).join("\n")
|
147
|
+
else
|
148
|
+
say_error("Couldn't insert file header for content: #{content} due to unexpected file format")
|
149
|
+
content
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/tapioca/commands/dsl.rb
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
module Tapioca
|
5
5
|
module Commands
|
6
6
|
class Dsl < Command
|
7
|
+
include SorbetHelper
|
8
|
+
include RBIHelper
|
9
|
+
|
7
10
|
sig do
|
8
11
|
params(
|
9
12
|
requested_constants: T::Array[String],
|
@@ -17,6 +20,8 @@ module Tapioca
|
|
17
20
|
quiet: T::Boolean,
|
18
21
|
verbose: T::Boolean,
|
19
22
|
number_of_workers: T.nilable(Integer),
|
23
|
+
auto_strictness: T::Boolean,
|
24
|
+
gem_dir: String,
|
20
25
|
rbi_formatter: RBIFormatter
|
21
26
|
).void
|
22
27
|
end
|
@@ -32,6 +37,8 @@ module Tapioca
|
|
32
37
|
quiet: false,
|
33
38
|
verbose: false,
|
34
39
|
number_of_workers: nil,
|
40
|
+
auto_strictness: true,
|
41
|
+
gem_dir: DEFAULT_GEM_DIR,
|
35
42
|
rbi_formatter: DEFAULT_RBI_FORMATTER
|
36
43
|
)
|
37
44
|
@requested_constants = requested_constants
|
@@ -45,6 +52,8 @@ module Tapioca
|
|
45
52
|
@quiet = quiet
|
46
53
|
@verbose = verbose
|
47
54
|
@number_of_workers = number_of_workers
|
55
|
+
@auto_strictness = auto_strictness
|
56
|
+
@gem_dir = gem_dir
|
48
57
|
@rbi_formatter = rbi_formatter
|
49
58
|
|
50
59
|
super()
|
@@ -102,9 +111,19 @@ module Tapioca
|
|
102
111
|
perform_dsl_verification(outpath)
|
103
112
|
else
|
104
113
|
purge_stale_dsl_rbi_files(rbi_files_to_purge)
|
105
|
-
|
106
114
|
say("Done", :green)
|
107
115
|
|
116
|
+
if @auto_strictness
|
117
|
+
say("")
|
118
|
+
validate_rbi_files(
|
119
|
+
command: default_command(:dsl, @requested_constants.join(" ")),
|
120
|
+
gem_dir: @gem_dir,
|
121
|
+
dsl_dir: @outpath.to_s,
|
122
|
+
auto_strictness: @auto_strictness,
|
123
|
+
compilers: pipeline.compilers
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
108
127
|
say("All operations performed in working directory.", [:green, :bold])
|
109
128
|
say("Please review changes and commit them.", [:green, :bold])
|
110
129
|
end
|
data/lib/tapioca/commands/gem.rb
CHANGED
@@ -5,6 +5,7 @@ module Tapioca
|
|
5
5
|
module Commands
|
6
6
|
class Gem < Command
|
7
7
|
include SorbetHelper
|
8
|
+
include RBIHelper
|
8
9
|
|
9
10
|
sig do
|
10
11
|
params(
|
@@ -78,7 +79,13 @@ module Tapioca
|
|
78
79
|
end
|
79
80
|
|
80
81
|
if anything_done
|
81
|
-
|
82
|
+
validate_rbi_files(
|
83
|
+
command: default_command(:gem, @gem_names.join(" ")),
|
84
|
+
gem_dir: @outpath.to_s,
|
85
|
+
dsl_dir: @dsl_dir,
|
86
|
+
auto_strictness: @auto_strictness,
|
87
|
+
gems: bundle.dependencies
|
88
|
+
)
|
82
89
|
|
83
90
|
say("All operations performed in working directory.", [:green, :bold])
|
84
91
|
say("Please review changes and commit them.", [:green, :bold])
|
@@ -102,7 +109,13 @@ module Tapioca
|
|
102
109
|
].any?
|
103
110
|
|
104
111
|
if anything_done
|
105
|
-
|
112
|
+
validate_rbi_files(
|
113
|
+
command: default_command(:gem),
|
114
|
+
gem_dir: @outpath.to_s,
|
115
|
+
dsl_dir: @dsl_dir,
|
116
|
+
auto_strictness: @auto_strictness,
|
117
|
+
gems: bundle.dependencies
|
118
|
+
)
|
106
119
|
|
107
120
|
say("All operations performed in working directory.", [:green, :bold])
|
108
121
|
say("Please review changes and commit them.", [:green, :bold])
|
@@ -122,7 +135,7 @@ module Tapioca
|
|
122
135
|
|
123
136
|
sig { returns(Gemfile) }
|
124
137
|
def bundle
|
125
|
-
@bundle ||= Gemfile.new
|
138
|
+
@bundle ||= Gemfile.new(@exclude)
|
126
139
|
end
|
127
140
|
|
128
141
|
sig { void }
|
@@ -359,6 +372,7 @@ module Tapioca
|
|
359
372
|
sig { params(gem: Gemfile::GemSpec, file: RBI::File).void }
|
360
373
|
def merge_with_exported_rbi(gem, file)
|
361
374
|
return file unless gem.export_rbi_files?
|
375
|
+
|
362
376
|
tree = gem.exported_rbi_tree
|
363
377
|
|
364
378
|
unless tree.conflicts.empty?
|
@@ -379,60 +393,6 @@ module Tapioca
|
|
379
393
|
say_error("\n\n RBIs exported by `#{gem.name}` contain errors and can't be used:", :yellow)
|
380
394
|
say_error("Cause: #{e.message} (#{e.location})")
|
381
395
|
end
|
382
|
-
|
383
|
-
sig { params(gem_names: T::Array[String], gem_dir: String, dsl_dir: String).void }
|
384
|
-
def update_strictnesses(gem_names, gem_dir: DEFAULT_GEM_DIR, dsl_dir: DEFAULT_DSL_DIR)
|
385
|
-
return unless File.directory?(dsl_dir)
|
386
|
-
|
387
|
-
error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
|
388
|
-
|
389
|
-
say("Typechecking RBI files... ")
|
390
|
-
res = sorbet(
|
391
|
-
"--no-config",
|
392
|
-
"--error-url-base=#{error_url_base}",
|
393
|
-
"--isolate-error-code 4010",
|
394
|
-
dsl_dir,
|
395
|
-
gem_dir
|
396
|
-
)
|
397
|
-
say(" Done", :green)
|
398
|
-
|
399
|
-
errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
|
400
|
-
|
401
|
-
if errors.empty?
|
402
|
-
say("No error found", [:green, :bold])
|
403
|
-
return
|
404
|
-
end
|
405
|
-
|
406
|
-
files = []
|
407
|
-
|
408
|
-
errors.each do |error|
|
409
|
-
# Collect the file with error
|
410
|
-
files << error.file
|
411
|
-
error.more.each do |line|
|
412
|
-
# Also collect the conflicting definition file paths
|
413
|
-
next unless line.include?("Previous definition")
|
414
|
-
files << line.split(":").first&.strip
|
415
|
-
end
|
416
|
-
end
|
417
|
-
|
418
|
-
files
|
419
|
-
.uniq
|
420
|
-
.sort
|
421
|
-
.select do |file|
|
422
|
-
name = gem_name_from_rbi_path(file)
|
423
|
-
file.start_with?(gem_dir) && (gem_names.empty? || gem_names.include?(name))
|
424
|
-
end.each do |file|
|
425
|
-
Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
|
426
|
-
say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
|
427
|
-
end
|
428
|
-
|
429
|
-
say("\n")
|
430
|
-
end
|
431
|
-
|
432
|
-
sig { params(path: String).returns(String) }
|
433
|
-
def gem_name_from_rbi_path(path)
|
434
|
-
T.must(File.basename(path, ".rbi").split("@").first)
|
435
|
-
end
|
436
396
|
end
|
437
397
|
end
|
438
398
|
end
|
data/lib/tapioca/commands.rb
CHANGED
@@ -83,6 +83,7 @@ module Tapioca
|
|
83
83
|
create_type_members(klass, "Key", "Value")
|
84
84
|
else
|
85
85
|
descriptor = T.let(T.unsafe(constant).descriptor, Google::Protobuf::Descriptor)
|
86
|
+
descriptor.each_oneof { |oneof| create_oneof_method(klass, oneof) }
|
86
87
|
fields = descriptor.map { |desc| create_descriptor_method(klass, desc) }
|
87
88
|
fields.sort_by!(&:name)
|
88
89
|
|
@@ -216,6 +217,19 @@ module Tapioca
|
|
216
217
|
|
217
218
|
field
|
218
219
|
end
|
220
|
+
|
221
|
+
sig do
|
222
|
+
params(
|
223
|
+
klass: RBI::Scope,
|
224
|
+
desc: Google::Protobuf::OneofDescriptor
|
225
|
+
).void
|
226
|
+
end
|
227
|
+
def create_oneof_method(klass, desc)
|
228
|
+
klass.create_method(
|
229
|
+
desc.name,
|
230
|
+
return_type: "T.nilable(Symbol)"
|
231
|
+
)
|
232
|
+
end
|
219
233
|
end
|
220
234
|
end
|
221
235
|
end
|
data/lib/tapioca/dsl/pipeline.rb
CHANGED
@@ -149,8 +149,12 @@ module Tapioca
|
|
149
149
|
|
150
150
|
compilers.each do |compiler_class|
|
151
151
|
next unless compiler_class.handles?(constant)
|
152
|
+
|
152
153
|
compiler = compiler_class.new(self, file.root, constant)
|
153
154
|
compiler.decorate
|
155
|
+
rescue
|
156
|
+
$stderr.puts("Error: `#{compiler_class.name}` failed to generate RBI for `#{constant}`")
|
157
|
+
raise # This is an unexpected error, so re-raise it
|
154
158
|
end
|
155
159
|
|
156
160
|
return if file.root.empty?
|
data/lib/tapioca/dsl.rb
ADDED
data/lib/tapioca/executor.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "etc"
|
5
|
+
require "parallel"
|
5
6
|
|
6
7
|
module Tapioca
|
7
8
|
class Executor
|
@@ -20,10 +21,6 @@ module Tapioca
|
|
20
21
|
number_of_workers || [Etc.nprocessors, (queue.length.to_f / MINIMUM_ITEMS_PER_WORKER).ceil].min,
|
21
22
|
Integer
|
22
23
|
)
|
23
|
-
|
24
|
-
# The number of items that will be processed per worker, so that we can split the queue into groups and assign
|
25
|
-
# them to each one of the workers
|
26
|
-
@items_per_worker = T.let((queue.length.to_f / @number_of_workers).ceil, Integer)
|
27
24
|
end
|
28
25
|
|
29
26
|
sig do
|
@@ -32,48 +29,9 @@ module Tapioca
|
|
32
29
|
).returns(T::Array[T.type_parameter(:T)])
|
33
30
|
end
|
34
31
|
def run_in_parallel(&block)
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
read_pipes = []
|
39
|
-
write_pipes = []
|
40
|
-
|
41
|
-
# If we have more than one worker, fork the pool by shifting the expected number of items per worker from the
|
42
|
-
# queue
|
43
|
-
workers = (0...@number_of_workers).map do
|
44
|
-
items = @queue.shift(@items_per_worker)
|
45
|
-
|
46
|
-
# Each worker has their own pair of pipes, so that we can read the result from each worker separately
|
47
|
-
read, write = IO.pipe
|
48
|
-
read_pipes << read
|
49
|
-
write_pipes << write
|
50
|
-
|
51
|
-
fork do
|
52
|
-
read.close
|
53
|
-
result = items.map { |item| block.call(item) }
|
54
|
-
|
55
|
-
# Pack the result as a Base64 string of the Marshal dump of the array of values returned by the block that we
|
56
|
-
# ran in parallel
|
57
|
-
packed = [Marshal.dump(result)].pack("m")
|
58
|
-
write.puts(packed)
|
59
|
-
write.close
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
# Close all the write pipes, then read and close from all the read pipes
|
64
|
-
write_pipes.each(&:close)
|
65
|
-
result = read_pipes.map do |pipe|
|
66
|
-
content = pipe.read
|
67
|
-
pipe.close
|
68
|
-
content
|
69
|
-
end
|
70
|
-
|
71
|
-
# Wait until all the workers finish. Notice that waiting for the PIDs can only happen after we read and close the
|
72
|
-
# pipe or else we may end up in a condition where writing to the pipe hangs indefinitely
|
73
|
-
workers.each { |pid| Process.waitpid(pid) }
|
74
|
-
|
75
|
-
# Decode the value back into the Ruby objects by doing the inverse of what each worker does
|
76
|
-
result.flat_map { |item| T.unsafe(Marshal.load(item.unpack1("m"))) }
|
32
|
+
# To have the parallel gem run jobs in the parent process, you must pass 0 as the number of processes
|
33
|
+
number_of_processes = @number_of_workers == 1 ? 0 : @number_of_workers
|
34
|
+
Parallel.map(@queue, { in_processes: number_of_processes }, &block)
|
77
35
|
end
|
78
36
|
end
|
79
37
|
end
|
@@ -41,6 +41,7 @@ module Tapioca
|
|
41
41
|
.each do |visibility, method_list|
|
42
42
|
method_list.sort!.map do |name|
|
43
43
|
next if name == :initialize
|
44
|
+
|
44
45
|
vis = case visibility
|
45
46
|
when :protected
|
46
47
|
RBI::Protected.new
|
@@ -65,7 +66,7 @@ module Tapioca
|
|
65
66
|
end
|
66
67
|
def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new)
|
67
68
|
return unless method
|
68
|
-
return unless method
|
69
|
+
return unless method_owned_by_constant?(method, constant)
|
69
70
|
return if @pipeline.symbol_in_payload?(symbol_name) && !@pipeline.method_in_gem?(method)
|
70
71
|
|
71
72
|
signature = signature_of(method)
|
@@ -141,6 +142,29 @@ module Tapioca
|
|
141
142
|
tree << rbi_method
|
142
143
|
end
|
143
144
|
|
145
|
+
# Check whether the method is defined by the constant.
|
146
|
+
#
|
147
|
+
# In most cases, it works to check that the constant is the method owner. However,
|
148
|
+
# in the case that a method is also defined in a module prepended to the constant, it
|
149
|
+
# will be owned by the prepended module, not the constant.
|
150
|
+
#
|
151
|
+
# This method implements a better way of checking whether a constant defines a method.
|
152
|
+
# It walks up the ancestor tree via the `super_method` method; if any of the super
|
153
|
+
# methods are owned by the constant, it means that the constant declares the method.
|
154
|
+
sig { params(method: UnboundMethod, constant: Module).returns(T::Boolean) }
|
155
|
+
def method_owned_by_constant?(method, constant)
|
156
|
+
# Widen the type of `method` to be nilable
|
157
|
+
method = T.let(method, T.nilable(UnboundMethod))
|
158
|
+
|
159
|
+
while method
|
160
|
+
return true if method.owner == constant
|
161
|
+
|
162
|
+
method = method.super_method
|
163
|
+
end
|
164
|
+
|
165
|
+
false
|
166
|
+
end
|
167
|
+
|
144
168
|
sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
|
145
169
|
def method_names_by_visibility(mod)
|
146
170
|
{
|
@@ -163,6 +187,7 @@ module Tapioca
|
|
163
187
|
sig { params(name: String).returns(T::Boolean) }
|
164
188
|
def valid_method_name?(name)
|
165
189
|
return true if SPECIAL_METHOD_NAMES.include?(name)
|
190
|
+
|
166
191
|
!!name.match(/^[[:word:]]+[?!=]?$/)
|
167
192
|
end
|
168
193
|
|
@@ -19,7 +19,7 @@ module Tapioca
|
|
19
19
|
constant.props.map do |name, prop|
|
20
20
|
type = prop.fetch(:type_object, "T.untyped").to_s.gsub(".returns(<VOID>)", ".void")
|
21
21
|
|
22
|
-
default = prop.key?(:default) ? "T.unsafe(nil)" : nil
|
22
|
+
default = prop.key?(:default) || prop.key?(:factory) ? "T.unsafe(nil)" : nil
|
23
23
|
node << if prop.fetch(:immutable, false)
|
24
24
|
RBI::TStructConst.new(name.to_s, type, default: default)
|
25
25
|
else
|
@@ -14,6 +14,7 @@ module Tapioca
|
|
14
14
|
ancestors = Runtime::Trackers::RequiredAncestor.required_ancestors_by(event.constant)
|
15
15
|
ancestors.each do |ancestor|
|
16
16
|
next unless ancestor # TODO: We should have a way to warn from here
|
17
|
+
|
17
18
|
event.node << RBI::RequiresAncestor.new(ancestor.to_s)
|
18
19
|
end
|
19
20
|
end
|
data/lib/tapioca/gem/pipeline.rb
CHANGED
@@ -87,6 +87,7 @@ module Tapioca
|
|
87
87
|
def symbol_in_payload?(symbol_name)
|
88
88
|
symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
|
89
89
|
return false unless symbol_name
|
90
|
+
|
90
91
|
@payload_symbols.include?(symbol_name)
|
91
92
|
end
|
92
93
|
|
@@ -102,9 +103,11 @@ module Tapioca
|
|
102
103
|
def name_of(constant)
|
103
104
|
name = name_of_proxy_target(constant, super(class_of(constant)))
|
104
105
|
return name if name
|
106
|
+
|
105
107
|
name = super(constant)
|
106
108
|
return if name.nil?
|
107
109
|
return unless are_equal?(constant, constantize(name, inherit: true))
|
110
|
+
|
108
111
|
name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
|
109
112
|
name
|
110
113
|
end
|
@@ -350,6 +353,7 @@ module Tapioca
|
|
350
353
|
sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
|
351
354
|
def name_of_proxy_target(constant, class_name)
|
352
355
|
return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
|
356
|
+
|
353
357
|
# We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
|
354
358
|
# so try to get the name of the target class
|
355
359
|
begin
|