tapioca 0.7.3 → 0.8.0
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.
- 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
|