tapioca 0.9.3 → 0.9.4
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/README.md +6 -0
- data/lib/tapioca/cli.rb +12 -4
- data/lib/tapioca/commands/annotations.rb +8 -5
- data/lib/tapioca/commands/check_shims.rb +41 -20
- data/lib/tapioca/commands/dsl.rb +19 -17
- data/lib/tapioca/commands/gem.rb +27 -21
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +24 -9
- data/lib/tapioca/dsl/compilers/frozen_record.rb +2 -2
- data/lib/tapioca/dsl/compilers/protobuf.rb +8 -0
- data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +20 -1
- data/lib/tapioca/gem/events.rb +6 -1
- data/lib/tapioca/gem/listeners/methods.rb +1 -1
- data/lib/tapioca/gem/listeners/source_location.rb +67 -0
- data/lib/tapioca/gem/listeners.rb +1 -0
- data/lib/tapioca/gem/pipeline.rb +7 -5
- data/lib/tapioca/helpers/config_helper.rb +13 -13
- data/lib/tapioca/helpers/rbi_files_helper.rb +45 -46
- data/lib/tapioca/helpers/rbi_helper.rb +26 -5
- data/lib/tapioca/helpers/test/template.rb +5 -0
- data/lib/tapioca/internal.rb +2 -0
- data/lib/tapioca/runtime/loader.rb +8 -2
- data/lib/tapioca/runtime/reflection.rb +5 -6
- data/lib/tapioca/runtime/trackers/constant_definition.rb +39 -21
- data/lib/tapioca/runtime/trackers/mixin.rb +1 -1
- data/lib/tapioca/sorbet_ext/name_patch.rb +15 -5
- data/lib/tapioca/sorbet_ext/proc_bind_patch.rb +40 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +12 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 212af3a2ba5204b2aa479e52acddf879f32755faaee95691ce51e5c26b306481
|
4
|
+
data.tar.gz: e8f957c96874e08fed7cce1580ac110c41bef0853c6f52f2e46c90bb6435bfe8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe339f96a20827fd80cb4134ccfcb258319ea43017f8b0f98170792e8732e464e468efeed7f7c0092fb095fff1e62e2f79f51113bbc6cff78e45b9cf288febc3
|
7
|
+
data.tar.gz: dafe9aeaa6118b98c1c4a5e40064a55aacf7bc883d9ffb406bca0edd8bfecacb52b7f1f0c898bb8b09fa2c3101c9a88e3ba812fbbd3f71c15837fb110b241e69
|
data/README.md
CHANGED
@@ -152,6 +152,8 @@ Options:
|
|
152
152
|
[--verify], [--no-verify] # Verify RBIs are up-to-date
|
153
153
|
[--doc], [--no-doc] # Include YARD documentation from sources when generating RBIs. Warning: this might be slow
|
154
154
|
# Default: true
|
155
|
+
[--loc], [--no-loc] # Include comments with source location when generating RBIs
|
156
|
+
# Default: true
|
155
157
|
[--exported-gem-rbis], [--no-exported-gem-rbis] # Include RBIs found in the `rbi/` directory of the gem
|
156
158
|
# Default: true
|
157
159
|
-w, [--workers=N] # EXPERIMENTAL: Number of parallel workers to use when generating RBIs
|
@@ -740,6 +742,8 @@ Options:
|
|
740
742
|
# Default: sorbet/rbi/todo.rbi
|
741
743
|
[--payload], [--no-payload] # Check shims against Sorbet's payload
|
742
744
|
# Default: true
|
745
|
+
-w, [--workers=N] # EXPERIMENTAL: Number of parallel workers
|
746
|
+
# Default: 1
|
743
747
|
-c, [--config=<config file path>] # Path to the Tapioca configuration file
|
744
748
|
# Default: sorbet/tapioca/config.yml
|
745
749
|
-V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
|
@@ -802,6 +806,7 @@ gem:
|
|
802
806
|
activesupport: 'false'
|
803
807
|
verify: false
|
804
808
|
doc: true
|
809
|
+
loc: true
|
805
810
|
exported_gem_rbis: true
|
806
811
|
workers: 1
|
807
812
|
auto_strictness: true
|
@@ -815,6 +820,7 @@ check_shims:
|
|
815
820
|
annotations_rbi_dir: sorbet/rbi/annotations
|
816
821
|
todo_rbi_file: sorbet/rbi/todo.rbi
|
817
822
|
payload: true
|
823
|
+
workers: 1
|
818
824
|
annotations:
|
819
825
|
sources:
|
820
826
|
- https://raw.githubusercontent.com/Shopify/rbi-central/main
|
data/lib/tapioca/cli.rb
CHANGED
@@ -190,6 +190,10 @@ module Tapioca
|
|
190
190
|
type: :boolean,
|
191
191
|
desc: "Include YARD documentation from sources when generating RBIs. Warning: this might be slow",
|
192
192
|
default: true
|
193
|
+
option :loc,
|
194
|
+
type: :boolean,
|
195
|
+
desc: "Include comments with source location when generating RBIs",
|
196
|
+
default: true
|
193
197
|
option :exported_gem_rbis,
|
194
198
|
type: :boolean,
|
195
199
|
desc: "Include RBIs found in the `rbi/` directory of the gem",
|
@@ -233,6 +237,7 @@ module Tapioca
|
|
233
237
|
outpath: Pathname.new(options[:outdir]),
|
234
238
|
file_header: options[:file_header],
|
235
239
|
include_doc: options[:doc],
|
240
|
+
include_loc: options[:loc],
|
236
241
|
include_exported_rbis: options[:exported_gem_rbis],
|
237
242
|
number_of_workers: options[:workers],
|
238
243
|
auto_strictness: options[:auto_strictness],
|
@@ -255,7 +260,7 @@ module Tapioca
|
|
255
260
|
end
|
256
261
|
|
257
262
|
if gems.empty? && !all
|
258
|
-
command.sync(should_verify: verify)
|
263
|
+
command.sync(should_verify: verify, exclude: options[:exclude])
|
259
264
|
else
|
260
265
|
command.execute
|
261
266
|
end
|
@@ -270,14 +275,18 @@ module Tapioca
|
|
270
275
|
option :annotations_rbi_dir, type: :string, desc: "Path to annotations RBIs", default: DEFAULT_ANNOTATIONS_DIR
|
271
276
|
option :todo_rbi_file, type: :string, desc: "Path to the generated todo RBI file", default: DEFAULT_TODO_FILE
|
272
277
|
option :payload, type: :boolean, desc: "Check shims against Sorbet's payload", default: true
|
278
|
+
option :workers, aliases: ["-w"], type: :numeric, desc: "EXPERIMENTAL: Number of parallel workers", default: 1
|
273
279
|
def check_shims
|
280
|
+
Tapioca.disable_traces
|
281
|
+
|
274
282
|
command = Commands::CheckShims.new(
|
275
283
|
gem_rbi_dir: options[:gem_rbi_dir],
|
276
284
|
dsl_rbi_dir: options[:dsl_rbi_dir],
|
277
285
|
shim_rbi_dir: options[:shim_rbi_dir],
|
278
286
|
annotations_rbi_dir: options[:annotations_rbi_dir],
|
279
287
|
todo_rbi_file: options[:todo_rbi_file],
|
280
|
-
payload: options[:payload]
|
288
|
+
payload: options[:payload],
|
289
|
+
number_of_workers: options[:workers]
|
281
290
|
)
|
282
291
|
command.execute
|
283
292
|
end
|
@@ -296,8 +305,7 @@ module Tapioca
|
|
296
305
|
default: {}
|
297
306
|
def annotations
|
298
307
|
if !options[:netrc] && options[:netrc_file]
|
299
|
-
|
300
|
-
exit(1)
|
308
|
+
raise Thor::Error, set_color("Options `--no-netrc` and `--netrc-file` can't be used together", :bold, :red)
|
301
309
|
end
|
302
310
|
|
303
311
|
command = Commands::Annotations.new(
|
@@ -88,8 +88,7 @@ module Tapioca
|
|
88
88
|
end
|
89
89
|
|
90
90
|
if indexes.empty?
|
91
|
-
|
92
|
-
exit(1)
|
91
|
+
raise Thor::Error, set_color("Can't fetch annotations without sources (no index fetched)", :bold, :red)
|
93
92
|
end
|
94
93
|
|
95
94
|
indexes
|
@@ -112,12 +111,15 @@ module Tapioca
|
|
112
111
|
fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
|
113
112
|
|
114
113
|
gem_names.each_with_object(fetchable_gems) do |gem_name, hash|
|
115
|
-
@indexes.each
|
114
|
+
@indexes.each do |uri, index|
|
115
|
+
T.must(hash[gem_name]) << uri if index.has_gem?(gem_name)
|
116
|
+
end
|
116
117
|
end
|
117
118
|
|
118
119
|
if fetchable_gems.empty?
|
119
120
|
say(" Nothing to do")
|
120
|
-
|
121
|
+
|
122
|
+
return []
|
121
123
|
end
|
122
124
|
|
123
125
|
say("\n")
|
@@ -193,7 +195,8 @@ module Tapioca
|
|
193
195
|
# Please run `#{default_command(:annotations)}` to update it.
|
194
196
|
COMMENT
|
195
197
|
|
196
|
-
contents
|
198
|
+
# Split contents into newlines and ensure trailing empty lines are included
|
199
|
+
contents = content.split("\n", -1)
|
197
200
|
if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
|
198
201
|
contents.insert(2, header).join("\n")
|
199
202
|
else
|
@@ -15,10 +15,19 @@ module Tapioca
|
|
15
15
|
annotations_rbi_dir: String,
|
16
16
|
shim_rbi_dir: String,
|
17
17
|
todo_rbi_file: String,
|
18
|
-
payload: T::Boolean
|
18
|
+
payload: T::Boolean,
|
19
|
+
number_of_workers: T.nilable(Integer)
|
19
20
|
).void
|
20
21
|
end
|
21
|
-
def initialize(
|
22
|
+
def initialize(
|
23
|
+
gem_rbi_dir:,
|
24
|
+
dsl_rbi_dir:,
|
25
|
+
annotations_rbi_dir:,
|
26
|
+
shim_rbi_dir:,
|
27
|
+
todo_rbi_file:,
|
28
|
+
payload:,
|
29
|
+
number_of_workers:
|
30
|
+
)
|
22
31
|
super()
|
23
32
|
@gem_rbi_dir = gem_rbi_dir
|
24
33
|
@dsl_rbi_dir = dsl_rbi_dir
|
@@ -26,6 +35,7 @@ module Tapioca
|
|
26
35
|
@shim_rbi_dir = shim_rbi_dir
|
27
36
|
@todo_rbi_file = todo_rbi_file
|
28
37
|
@payload = payload
|
38
|
+
@number_of_workers = number_of_workers
|
29
39
|
end
|
30
40
|
|
31
41
|
sig { override.void }
|
@@ -34,7 +44,8 @@ module Tapioca
|
|
34
44
|
|
35
45
|
if (!Dir.exist?(@shim_rbi_dir) || Dir.empty?(@shim_rbi_dir)) && !File.exist?(@todo_rbi_file)
|
36
46
|
say("No shim RBIs to check", :green)
|
37
|
-
|
47
|
+
|
48
|
+
return
|
38
49
|
end
|
39
50
|
|
40
51
|
payload_path = T.let(nil, T.nilable(String))
|
@@ -46,45 +57,55 @@ module Tapioca
|
|
46
57
|
result = sorbet("--no-config --print=payload-sources:#{payload_path}")
|
47
58
|
|
48
59
|
unless result.status
|
49
|
-
|
50
|
-
|
51
|
-
|
60
|
+
raise Thor::Error, <<~ERROR
|
61
|
+
"Sorbet failed to dump payload"
|
62
|
+
#{result.err}
|
63
|
+
ERROR
|
52
64
|
end
|
53
65
|
|
54
|
-
|
66
|
+
index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
|
55
67
|
end
|
56
68
|
else
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
69
|
+
raise Thor::Error, <<~ERROR
|
70
|
+
The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`
|
71
|
+
Current: v#{SORBET_GEM_SPEC.version}
|
72
|
+
Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}
|
73
|
+
ERROR
|
61
74
|
end
|
62
75
|
end
|
63
76
|
|
64
77
|
index_rbi(index, "todo", @todo_rbi_file)
|
65
|
-
index_rbis(index, "shim", @shim_rbi_dir)
|
66
|
-
index_rbis(index, "gem", @gem_rbi_dir)
|
67
|
-
index_rbis(index, "dsl", @dsl_rbi_dir)
|
68
|
-
index_rbis(index, "annotation", @annotations_rbi_dir)
|
78
|
+
index_rbis(index, "shim", @shim_rbi_dir, number_of_workers: @number_of_workers)
|
79
|
+
index_rbis(index, "gem", @gem_rbi_dir, number_of_workers: @number_of_workers)
|
80
|
+
index_rbis(index, "dsl", @dsl_rbi_dir, number_of_workers: @number_of_workers)
|
81
|
+
index_rbis(index, "annotation", @annotations_rbi_dir, number_of_workers: @number_of_workers)
|
69
82
|
|
70
83
|
duplicates = duplicated_nodes_from_index(index, shim_rbi_dir: @shim_rbi_dir, todo_rbi_file: @todo_rbi_file)
|
84
|
+
|
71
85
|
unless duplicates.empty?
|
86
|
+
messages = []
|
87
|
+
|
72
88
|
duplicates.each do |key, nodes|
|
73
|
-
|
89
|
+
messages << set_color("\nDuplicated RBI for #{key}:", :red)
|
90
|
+
|
74
91
|
nodes.each do |node|
|
75
92
|
node_loc = node.loc
|
93
|
+
|
76
94
|
next unless node_loc
|
77
95
|
|
78
96
|
loc_string = location_to_payload_url(node_loc, path_prefix: payload_path)
|
79
|
-
|
97
|
+
messages << set_color(" * #{loc_string}", :red)
|
80
98
|
end
|
81
99
|
end
|
82
|
-
|
83
|
-
|
100
|
+
|
101
|
+
messages << set_color(
|
102
|
+
"\nPlease remove the duplicated definitions from #{@shim_rbi_dir} and #{@todo_rbi_file}", :red
|
103
|
+
)
|
104
|
+
|
105
|
+
raise Thor::Error, messages.join("\n")
|
84
106
|
end
|
85
107
|
|
86
108
|
say("\nNo duplicates found in shim RBIs", :green)
|
87
|
-
exit(0)
|
88
109
|
end
|
89
110
|
end
|
90
111
|
end
|
data/lib/tapioca/commands/dsl.rb
CHANGED
@@ -191,6 +191,7 @@ module Tapioca
|
|
191
191
|
end
|
192
192
|
|
193
193
|
unprocessable_constants = constant_map.select { |_, v| v.nil? }
|
194
|
+
|
194
195
|
unless unprocessable_constants.empty?
|
195
196
|
unprocessable_constants.each do |name, _|
|
196
197
|
say("Error: Cannot find constant '#{name}'", :red)
|
@@ -198,7 +199,7 @@ module Tapioca
|
|
198
199
|
remove_file(filename) if File.file?(filename)
|
199
200
|
end
|
200
201
|
|
201
|
-
|
202
|
+
raise Thor::Error, ""
|
202
203
|
end
|
203
204
|
|
204
205
|
constant_map.values
|
@@ -211,12 +212,13 @@ module Tapioca
|
|
211
212
|
end
|
212
213
|
|
213
214
|
unprocessable_compilers = compiler_map.select { |_, v| v.nil? }
|
215
|
+
|
214
216
|
unless unprocessable_compilers.empty?
|
215
|
-
unprocessable_compilers.
|
216
|
-
|
217
|
-
end
|
217
|
+
message = unprocessable_compilers.map do |name, _|
|
218
|
+
set_color("Error: Cannot find compiler '#{name}'", :red)
|
219
|
+
end.join("\n")
|
218
220
|
|
219
|
-
|
221
|
+
raise Thor::Error, message
|
220
222
|
end
|
221
223
|
|
222
224
|
T.cast(compiler_map.values, T::Array[T.class_of(Tapioca::Dsl::Compiler)])
|
@@ -332,18 +334,18 @@ module Tapioca
|
|
332
334
|
if diff.empty?
|
333
335
|
say("Nothing to do, all RBIs are up-to-date.")
|
334
336
|
else
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
337
|
+
reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
|
338
|
+
build_error_for_files(cause, diff_for_cause.map(&:first))
|
339
|
+
end.join("\n")
|
340
|
+
|
341
|
+
raise Thor::Error, <<~ERROR
|
342
|
+
#{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
|
343
|
+
#{set_color("`#{default_command(command)}`", [:green, :bold])}
|
344
|
+
#{set_color("Once it is complete, be sure to commit and push any changes", :green)}
|
345
|
+
|
346
|
+
#{set_color("Reason:", :red)}
|
347
|
+
#{reasons}
|
348
|
+
ERROR
|
347
349
|
end
|
348
350
|
end
|
349
351
|
|
data/lib/tapioca/commands/gem.rb
CHANGED
@@ -17,6 +17,7 @@ module Tapioca
|
|
17
17
|
outpath: Pathname,
|
18
18
|
file_header: T::Boolean,
|
19
19
|
include_doc: T::Boolean,
|
20
|
+
include_loc: T::Boolean,
|
20
21
|
include_exported_rbis: T::Boolean,
|
21
22
|
number_of_workers: T.nilable(Integer),
|
22
23
|
auto_strictness: T::Boolean,
|
@@ -33,6 +34,7 @@ module Tapioca
|
|
33
34
|
outpath:,
|
34
35
|
file_header:,
|
35
36
|
include_doc:,
|
37
|
+
include_loc:,
|
36
38
|
include_exported_rbis:,
|
37
39
|
number_of_workers: nil,
|
38
40
|
auto_strictness: true,
|
@@ -58,6 +60,7 @@ module Tapioca
|
|
58
60
|
@existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
59
61
|
@expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
60
62
|
@include_doc = T.let(include_doc, T::Boolean)
|
63
|
+
@include_loc = T.let(include_loc, T::Boolean)
|
61
64
|
@include_exported_rbis = include_exported_rbis
|
62
65
|
end
|
63
66
|
|
@@ -94,12 +97,12 @@ module Tapioca
|
|
94
97
|
end
|
95
98
|
end
|
96
99
|
|
97
|
-
sig { params(should_verify: T::Boolean).void }
|
98
|
-
def sync(should_verify: false)
|
100
|
+
sig { params(should_verify: T::Boolean, exclude: T::Array[String]).void }
|
101
|
+
def sync(should_verify: false, exclude: [])
|
99
102
|
if should_verify
|
100
103
|
say("Checking for out-of-date RBIs...")
|
101
104
|
say("")
|
102
|
-
perform_sync_verification
|
105
|
+
perform_sync_verification(exclude: exclude)
|
103
106
|
return
|
104
107
|
end
|
105
108
|
|
@@ -163,11 +166,12 @@ module Tapioca
|
|
163
166
|
return bundle.dependencies if gem_names.empty?
|
164
167
|
|
165
168
|
gem_names.map do |gem_name|
|
166
|
-
gem = bundle.gem(gem_name)
|
169
|
+
gem = @bundle.gem(gem_name)
|
170
|
+
|
167
171
|
if gem.nil?
|
168
|
-
|
169
|
-
exit(1)
|
172
|
+
raise Thor::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red)
|
170
173
|
end
|
174
|
+
|
171
175
|
gem
|
172
176
|
end
|
173
177
|
end
|
@@ -182,7 +186,7 @@ module Tapioca
|
|
182
186
|
default_command(:gem, gem.name),
|
183
187
|
reason: "types exported from the `#{gem.name}` gem",) if @file_header
|
184
188
|
|
185
|
-
rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc).compile
|
189
|
+
rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc, include_loc: @include_loc).compile
|
186
190
|
|
187
191
|
merge_with_exported_rbi(gem, rbi) if @include_exported_rbis
|
188
192
|
|
@@ -201,11 +205,13 @@ module Tapioca
|
|
201
205
|
end
|
202
206
|
end
|
203
207
|
|
204
|
-
sig { void }
|
205
|
-
def perform_sync_verification
|
208
|
+
sig { params(exclude: T::Array[String]).void }
|
209
|
+
def perform_sync_verification(exclude: [])
|
206
210
|
diff = {}
|
207
211
|
|
208
212
|
removed_rbis.each do |gem_name|
|
213
|
+
next if exclude.include?(gem_name)
|
214
|
+
|
209
215
|
filename = existing_rbi(gem_name)
|
210
216
|
diff[filename] = :removed
|
211
217
|
end
|
@@ -325,18 +331,18 @@ module Tapioca
|
|
325
331
|
if diff.empty?
|
326
332
|
say("Nothing to do, all RBIs are up-to-date.")
|
327
333
|
else
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
334
|
+
reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
|
335
|
+
build_error_for_files(cause, diff_for_cause.map(&:first))
|
336
|
+
end.join("\n")
|
337
|
+
|
338
|
+
raise Thor::Error, <<~ERROR
|
339
|
+
#{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
|
340
|
+
#{set_color("`#{default_command(command)}`", [:green, :bold])}
|
341
|
+
#{set_color("Once it is complete, be sure to commit and push any changes", :green)}
|
342
|
+
|
343
|
+
#{set_color("Reason:", :red)}
|
344
|
+
#{reasons}
|
345
|
+
ERROR
|
340
346
|
end
|
341
347
|
end
|
342
348
|
|
@@ -42,13 +42,12 @@ module Tapioca
|
|
42
42
|
|
43
43
|
sig { override.void }
|
44
44
|
def decorate
|
45
|
-
method_names = fixture_loader.
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
.flatten # merge methods into a single list
|
45
|
+
method_names = if fixture_loader.respond_to?(:fixture_sets)
|
46
|
+
method_names_from_lazy_fixture_loader
|
47
|
+
else
|
48
|
+
method_names_from_eager_fixture_loader
|
49
|
+
end
|
50
|
+
|
52
51
|
return if method_names.empty?
|
53
52
|
|
54
53
|
root.create_path(constant) do |mod|
|
@@ -69,11 +68,27 @@ module Tapioca
|
|
69
68
|
|
70
69
|
sig { returns(Class) }
|
71
70
|
def fixture_loader
|
72
|
-
Class.new do
|
71
|
+
@fixture_loader ||= T.let(Class.new do
|
73
72
|
T.unsafe(self).include(ActiveRecord::TestFixtures)
|
74
73
|
T.unsafe(self).fixture_path = Rails.root.join("test", "fixtures")
|
75
74
|
T.unsafe(self).fixtures(:all)
|
76
|
-
end
|
75
|
+
end, T.nilable(Class))
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(T::Array[String]) }
|
79
|
+
def method_names_from_lazy_fixture_loader
|
80
|
+
T.unsafe(fixture_loader).fixture_sets.keys
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { returns(T::Array[Symbol]) }
|
84
|
+
def method_names_from_eager_fixture_loader
|
85
|
+
fixture_loader.ancestors # get all ancestors from class that includes AR fixtures
|
86
|
+
.drop(1) # drop the anonymous class itself from the array
|
87
|
+
.reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
|
88
|
+
.map! do |mod|
|
89
|
+
[mod.private_instance_methods(false), mod.instance_methods(false)]
|
90
|
+
end
|
91
|
+
.flatten # merge methods into a single list
|
77
92
|
end
|
78
93
|
|
79
94
|
sig { params(mod: RBI::Scope, name: String).void }
|
@@ -65,7 +65,7 @@ module Tapioca
|
|
65
65
|
class FrozenRecord < Compiler
|
66
66
|
extend T::Sig
|
67
67
|
|
68
|
-
ConstantType = type_member { { fixed: T.class_of(::FrozenRecord::Base) } }
|
68
|
+
ConstantType = type_member { { fixed: T.all(T.class_of(::FrozenRecord::Base), Extensions::FrozenRecord) } }
|
69
69
|
|
70
70
|
sig { override.void }
|
71
71
|
def decorate
|
@@ -97,7 +97,7 @@ module Tapioca
|
|
97
97
|
|
98
98
|
sig { params(record: RBI::Scope).void }
|
99
99
|
def decorate_scopes(record)
|
100
|
-
scopes =
|
100
|
+
scopes = constant.__tapioca_scope_names
|
101
101
|
return if scopes.nil?
|
102
102
|
|
103
103
|
module_name = "GeneratedRelationMethods"
|
@@ -60,6 +60,14 @@ module Tapioca
|
|
60
60
|
# def number_value=(value); end
|
61
61
|
# end
|
62
62
|
# ~~~
|
63
|
+
#
|
64
|
+
# Please note that you might have to ignore the originally generated Protobuf Ruby files
|
65
|
+
# to avoid _Redefining constant_ issues when doing type checking.
|
66
|
+
# Do this by extending your Sorbet config file:
|
67
|
+
#
|
68
|
+
# ~~~
|
69
|
+
# --ignore=/path/to/proto/cart_pb.rb
|
70
|
+
# ~~~
|
63
71
|
class Protobuf < Compiler
|
64
72
|
class Field < T::Struct
|
65
73
|
prop :name, String
|
@@ -38,12 +38,20 @@ module Tapioca
|
|
38
38
|
"::Time"
|
39
39
|
when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
|
40
40
|
"::ActiveSupport::TimeWithZone"
|
41
|
+
when ActiveRecord::Enum::EnumType
|
42
|
+
"::String"
|
41
43
|
else
|
42
44
|
handle_unknown_type(column_type)
|
43
45
|
end
|
44
46
|
|
45
47
|
column = @constant.columns_hash[column_name]
|
46
|
-
setter_type =
|
48
|
+
setter_type =
|
49
|
+
case column_type
|
50
|
+
when ActiveRecord::Enum::EnumType
|
51
|
+
enum_setter_type(column_type)
|
52
|
+
else
|
53
|
+
getter_type
|
54
|
+
end
|
47
55
|
|
48
56
|
if column&.null
|
49
57
|
return [as_nilable_type(getter_type), as_nilable_type(setter_type)]
|
@@ -108,6 +116,17 @@ module Tapioca
|
|
108
116
|
|
109
117
|
first_argument_type.to_s
|
110
118
|
end
|
119
|
+
|
120
|
+
sig { params(column_type: ActiveRecord::Enum::EnumType).returns(String) }
|
121
|
+
def enum_setter_type(column_type)
|
122
|
+
# In Rails < 7 this method is private. When support for that is dropped we can call the method directly
|
123
|
+
case column_type.send(:subtype)
|
124
|
+
when ActiveRecord::Type::Integer
|
125
|
+
"T.any(::String, ::Symbol, ::Integer)"
|
126
|
+
else
|
127
|
+
"T.any(::String, ::Symbol)"
|
128
|
+
end
|
129
|
+
end
|
111
130
|
end
|
112
131
|
end
|
113
132
|
end
|
data/lib/tapioca/gem/events.rb
CHANGED
@@ -105,6 +105,9 @@ module Tapioca
|
|
105
105
|
class MethodNodeAdded < NodeAdded
|
106
106
|
extend T::Sig
|
107
107
|
|
108
|
+
sig { returns(UnboundMethod) }
|
109
|
+
attr_reader :method
|
110
|
+
|
108
111
|
sig { returns(RBI::Method) }
|
109
112
|
attr_reader :node
|
110
113
|
|
@@ -118,14 +121,16 @@ module Tapioca
|
|
118
121
|
params(
|
119
122
|
symbol: String,
|
120
123
|
constant: Module,
|
124
|
+
method: UnboundMethod,
|
121
125
|
node: RBI::Method,
|
122
126
|
signature: T.untyped,
|
123
127
|
parameters: T::Array[[Symbol, String]]
|
124
128
|
).void.checked(:never)
|
125
129
|
end
|
126
|
-
def initialize(symbol, constant, node, signature, parameters)
|
130
|
+
def initialize(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
|
127
131
|
super(symbol, constant)
|
128
132
|
@node = node
|
133
|
+
@method = method
|
129
134
|
@signature = signature
|
130
135
|
@parameters = parameters
|
131
136
|
end
|
@@ -134,7 +134,7 @@ module Tapioca
|
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
137
|
-
@pipeline.push_method(symbol_name, constant, rbi_method, signature, sanitized_parameters)
|
137
|
+
@pipeline.push_method(symbol_name, constant, method, rbi_method, signature, sanitized_parameters)
|
138
138
|
tree << rbi_method
|
139
139
|
end
|
140
140
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Gem
|
6
|
+
module Listeners
|
7
|
+
class SourceLocation < Base
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
sig { override.params(event: ConstNodeAdded).void }
|
13
|
+
def on_const(event)
|
14
|
+
file, line = Object.const_source_location(event.symbol)
|
15
|
+
add_source_location_comment(event.node, file, line)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { override.params(event: ScopeNodeAdded).void }
|
19
|
+
def on_scope(event)
|
20
|
+
# Instead of using `const_source_location`, which always reports the first place where a constant is defined,
|
21
|
+
# we filter the locations tracked by ConstantDefinition. This allows us to provide the correct location for
|
22
|
+
# constants that are defined by multiple gems.
|
23
|
+
locations = Runtime::Trackers::ConstantDefinition.locations_for(event.constant)
|
24
|
+
location = locations.find do |loc|
|
25
|
+
Pathname.new(loc.path).realpath.to_s.include?(@pipeline.gem.full_gem_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# The location may still be nil in some situations, like constant aliases (e.g.: MyAlias = OtherConst). These
|
29
|
+
# are quite difficult to attribute a correct location, given that the source location points to the original
|
30
|
+
# constants and not the alias
|
31
|
+
add_source_location_comment(event.node, location.path, location.lineno) unless location.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { override.params(event: MethodNodeAdded).void }
|
35
|
+
def on_method(event)
|
36
|
+
file, line = event.method.source_location
|
37
|
+
add_source_location_comment(event.node, file, line)
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(node: RBI::NodeWithComments, file: T.nilable(String), line: T.nilable(Integer)).void }
|
41
|
+
def add_source_location_comment(node, file, line)
|
42
|
+
return unless file && line
|
43
|
+
|
44
|
+
gem = @pipeline.gem
|
45
|
+
path = Pathname.new(file)
|
46
|
+
return unless File.exist?(path)
|
47
|
+
|
48
|
+
# On native extensions, the source location may point to a shared object (.so, .bundle) file, which we cannot
|
49
|
+
# use for jump to definition. Only add source comments on Ruby files
|
50
|
+
return unless path.extname == ".rb"
|
51
|
+
|
52
|
+
path = if path.realpath.to_s.start_with?(gem.full_gem_path)
|
53
|
+
"#{gem.name}-#{gem.version}/#{path.realpath.relative_path_from(gem.full_gem_path)}"
|
54
|
+
else
|
55
|
+
path.sub("#{Bundler.bundle_path}/gems/", "").to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
# Strip out the RUBY_ROOT prefix, which is different for each user
|
59
|
+
path = path.sub(RbConfig::CONFIG["rubylibdir"], "RUBY_ROOT")
|
60
|
+
|
61
|
+
node.comments << RBI::Comment.new("") if node.comments.any?
|
62
|
+
node.comments << RBI::Comment.new("source://#{path}:#{line}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/tapioca/gem/pipeline.rb
CHANGED
@@ -13,8 +13,8 @@ module Tapioca
|
|
13
13
|
sig { returns(Gemfile::GemSpec) }
|
14
14
|
attr_reader :gem
|
15
15
|
|
16
|
-
sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean).void }
|
17
|
-
def initialize(gem, include_doc: false)
|
16
|
+
sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean, include_loc: T::Boolean).void }
|
17
|
+
def initialize(gem, include_doc: false, include_loc: false)
|
18
18
|
@root = T.let(RBI::Tree.new, RBI::Tree)
|
19
19
|
@gem = gem
|
20
20
|
@seen = T.let(Set.new, T::Set[String])
|
@@ -40,6 +40,7 @@ module Tapioca
|
|
40
40
|
@node_listeners << Gem::Listeners::Subconstants.new(self)
|
41
41
|
@node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
|
42
42
|
@node_listeners << Gem::Listeners::ForeignConstants.new(self)
|
43
|
+
@node_listeners << Gem::Listeners::SourceLocation.new(self) if include_loc
|
43
44
|
@node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
|
44
45
|
end
|
45
46
|
|
@@ -87,13 +88,14 @@ module Tapioca
|
|
87
88
|
params(
|
88
89
|
symbol: String,
|
89
90
|
constant: Module,
|
91
|
+
method: UnboundMethod,
|
90
92
|
node: RBI::Method,
|
91
93
|
signature: T.untyped,
|
92
94
|
parameters: T::Array[[Symbol, String]]
|
93
95
|
).void.checked(:never)
|
94
96
|
end
|
95
|
-
def push_method(symbol, constant, node, signature, parameters)
|
96
|
-
@events << Gem::MethodNodeAdded.new(symbol, constant, node, signature, parameters)
|
97
|
+
def push_method(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
|
98
|
+
@events << Gem::MethodNodeAdded.new(symbol, constant, method, node, signature, parameters)
|
97
99
|
end
|
98
100
|
|
99
101
|
sig { params(symbol_name: String).returns(T::Boolean) }
|
@@ -353,7 +355,7 @@ module Tapioca
|
|
353
355
|
def get_file_candidates(constant)
|
354
356
|
wrapped_module = Pry::WrappedModule.new(constant)
|
355
357
|
|
356
|
-
wrapped_module.
|
358
|
+
wrapped_module.send(:method_candidates).flatten.filter_map(&:source_file).uniq
|
357
359
|
rescue ArgumentError, NameError
|
358
360
|
[]
|
359
361
|
end
|
@@ -86,8 +86,7 @@ module Tapioca
|
|
86
86
|
end.compact
|
87
87
|
|
88
88
|
unless errors.empty?
|
89
|
-
|
90
|
-
exit(1)
|
89
|
+
raise Thor::Error, build_error_message(config_file, errors)
|
91
90
|
end
|
92
91
|
ensure
|
93
92
|
@validating_config = false
|
@@ -173,18 +172,19 @@ module Tapioca
|
|
173
172
|
)
|
174
173
|
end
|
175
174
|
|
176
|
-
sig { params(config_file: String, errors: T::Array[ConfigError]).
|
177
|
-
def
|
178
|
-
|
179
|
-
|
180
|
-
|
175
|
+
sig { params(config_file: String, errors: T::Array[ConfigError]).returns(String) }
|
176
|
+
def build_error_message(config_file, errors)
|
177
|
+
error_messages = errors.map do |error|
|
178
|
+
"- " + error.message_parts.map do |part|
|
179
|
+
T.unsafe(self).set_color(part.message, *part.colors)
|
180
|
+
end.join
|
181
|
+
end.join("\n")
|
181
182
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
end
|
183
|
+
<<~ERROR
|
184
|
+
#{set_color("\nConfiguration file", :red)} #{set_color(config_file, :blue, :bold)} #{set_color("has the following errors:", :red)}
|
185
|
+
|
186
|
+
#{error_messages}
|
187
|
+
ERROR
|
188
188
|
end
|
189
189
|
|
190
190
|
sig do
|
@@ -9,33 +9,33 @@ module Tapioca
|
|
9
9
|
requires_ancestor { Thor::Shell }
|
10
10
|
requires_ancestor { SorbetHelper }
|
11
11
|
|
12
|
-
sig { params(index: RBI::Index, dir: String).void }
|
13
|
-
def index_payload(index, dir)
|
14
|
-
return unless Dir.exist?(dir)
|
15
|
-
|
16
|
-
say("Loading Sorbet payload... ")
|
17
|
-
files = Dir.glob("#{dir}/**/*.rbi").sort
|
18
|
-
parse_and_index_files(index, files)
|
19
|
-
say(" Done", :green)
|
20
|
-
end
|
21
|
-
|
22
12
|
sig { params(index: RBI::Index, kind: String, file: String).void }
|
23
13
|
def index_rbi(index, kind, file)
|
24
14
|
return unless File.exist?(file)
|
25
15
|
|
26
16
|
say("Loading #{kind} RBIs from #{file}... ")
|
27
|
-
|
28
|
-
|
17
|
+
time = Benchmark.realtime do
|
18
|
+
parse_and_index_files(index, [file], number_of_workers: 1)
|
19
|
+
end
|
20
|
+
say(" Done ", :green)
|
21
|
+
say("(#{time.round(2)}s)")
|
29
22
|
end
|
30
23
|
|
31
|
-
sig { params(index: RBI::Index, kind: String, dir: String).void }
|
32
|
-
def index_rbis(index, kind, dir)
|
24
|
+
sig { params(index: RBI::Index, kind: String, dir: String, number_of_workers: T.nilable(Integer)).void }
|
25
|
+
def index_rbis(index, kind, dir, number_of_workers:)
|
33
26
|
return unless Dir.exist?(dir) && !Dir.empty?(dir)
|
34
27
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
28
|
+
if kind == "payload"
|
29
|
+
say("Loading Sorbet payload... ")
|
30
|
+
else
|
31
|
+
say("Loading #{kind} RBIs from #{dir}... ")
|
32
|
+
end
|
33
|
+
time = Benchmark.realtime do
|
34
|
+
files = Dir.glob("#{dir}/**/*.rbi").sort
|
35
|
+
parse_and_index_files(index, files, number_of_workers: number_of_workers)
|
36
|
+
end
|
37
|
+
say(" Done ", :green)
|
38
|
+
say("(#{time.round(2)}s)")
|
39
39
|
end
|
40
40
|
|
41
41
|
sig do
|
@@ -48,13 +48,16 @@ module Tapioca
|
|
48
48
|
def duplicated_nodes_from_index(index, shim_rbi_dir:, todo_rbi_file:)
|
49
49
|
duplicates = {}
|
50
50
|
say("Looking for duplicates... ")
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
time = Benchmark.realtime do
|
52
|
+
index.keys.each do |key|
|
53
|
+
nodes = index[key]
|
54
|
+
next unless shims_or_todos_have_duplicates?(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
|
54
55
|
|
55
|
-
|
56
|
+
duplicates[key] = nodes
|
57
|
+
end
|
56
58
|
end
|
57
|
-
say(" Done", :green)
|
59
|
+
say(" Done ", :green)
|
60
|
+
say("(#{time.round(2)}s)")
|
58
61
|
duplicates
|
59
62
|
end
|
60
63
|
|
@@ -97,14 +100,16 @@ module Tapioca
|
|
97
100
|
|
98
101
|
if errors.empty?
|
99
102
|
say(" No errors found\n\n", [:green, :bold])
|
103
|
+
|
100
104
|
return
|
101
105
|
end
|
102
106
|
|
103
107
|
parse_errors = errors.select { |error| error.code < 4000 }
|
104
108
|
|
105
|
-
|
106
|
-
say_error(<<~ERR, :red)
|
109
|
+
error_messages = []
|
107
110
|
|
111
|
+
if parse_errors.any?
|
112
|
+
error_messages << set_color(<<~ERR, :red)
|
108
113
|
##### INTERNAL ERROR #####
|
109
114
|
|
110
115
|
There are parse errors in the generated RBI files.
|
@@ -116,27 +121,23 @@ module Tapioca
|
|
116
121
|
|
117
122
|
Command:
|
118
123
|
#{command}
|
119
|
-
|
120
124
|
ERR
|
121
125
|
|
122
|
-
|
126
|
+
error_messages << set_color(<<~ERR, :red) if gems.any?
|
123
127
|
Gems:
|
124
128
|
#{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
|
125
|
-
|
126
129
|
ERR
|
127
130
|
|
128
|
-
|
131
|
+
error_messages << set_color(<<~ERR, :red) if compilers.any?
|
129
132
|
Compilers:
|
130
133
|
#{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
|
131
|
-
|
132
134
|
ERR
|
133
135
|
|
134
|
-
|
136
|
+
error_messages << set_color(<<~ERR, :red)
|
135
137
|
Errors:
|
136
138
|
#{parse_errors.map { |error| " #{error}" }.join("\n")}
|
137
139
|
|
138
140
|
##########################
|
139
|
-
|
140
141
|
ERR
|
141
142
|
end
|
142
143
|
|
@@ -145,26 +146,24 @@ module Tapioca
|
|
145
146
|
update_gem_rbis_strictnesses(redef_errors, gem_dir)
|
146
147
|
end
|
147
148
|
|
148
|
-
Kernel.
|
149
|
+
Kernel.raise Thor::Error, error_messages.join("\n") if parse_errors.any?
|
149
150
|
end
|
150
151
|
|
151
152
|
private
|
152
153
|
|
153
|
-
sig { params(index: RBI::Index, files: T::Array[String]).void }
|
154
|
-
def parse_and_index_files(index, files)
|
155
|
-
files
|
156
|
-
parse_and_index_file(index, file)
|
157
|
-
end
|
158
|
-
end
|
154
|
+
sig { params(index: RBI::Index, files: T::Array[String], number_of_workers: T.nilable(Integer)).void }
|
155
|
+
def parse_and_index_files(index, files, number_of_workers:)
|
156
|
+
executor = Executor.new(files, number_of_workers: number_of_workers)
|
159
157
|
|
160
|
-
|
161
|
-
|
162
|
-
|
158
|
+
trees = executor.run_in_parallel do |file|
|
159
|
+
next if Spoom::Sorbet::Sigils.file_strictness(file) == "ignore"
|
160
|
+
|
161
|
+
RBI::Parser.parse_file(file)
|
162
|
+
rescue RBI::ParseError => e
|
163
|
+
say_error("\nWarning: #{e} (#{e.location})", :yellow)
|
164
|
+
end
|
163
165
|
|
164
|
-
|
165
|
-
index.visit(tree)
|
166
|
-
rescue RBI::ParseError => e
|
167
|
-
say_error("\nWarning: #{e} (#{e.location})", :yellow)
|
166
|
+
index.visit_all(trees)
|
168
167
|
end
|
169
168
|
|
170
169
|
sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
|
@@ -93,15 +93,36 @@ module Tapioca
|
|
93
93
|
|
94
94
|
sig { params(name: String).returns(T::Boolean) }
|
95
95
|
def valid_method_name?(name)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
)
|
96
|
+
# try to parse a method definition with this name
|
97
|
+
iseq = RubyVM::InstructionSequence.compile("def #{name}; end", nil, nil, 0, false)
|
98
|
+
# pull out the first operation in the instruction sequence and its first argument
|
99
|
+
op, arg, _data = iseq.to_a.dig(-1, 0)
|
100
|
+
# make sure that the operation is a method definition and the method that was
|
101
|
+
# defined has the expected name, for example, for `def !foo; end` we don't get
|
102
|
+
# a syntax error but instead get a method defined as `"foo"`
|
103
|
+
op == :definemethod && arg == name.to_sym
|
104
|
+
rescue SyntaxError
|
105
|
+
false
|
100
106
|
end
|
101
107
|
|
102
108
|
sig { params(name: String).returns(T::Boolean) }
|
103
109
|
def valid_parameter_name?(name)
|
104
|
-
|
110
|
+
sentinel_method_name = :sentinel_method_name
|
111
|
+
# try to parse a method definition with this name as the name of a
|
112
|
+
# keyword parameter. If we use a positional parameter, then parameter names
|
113
|
+
# like `&` (and maybe others) will be treated like `def foo(&); end` and will
|
114
|
+
# thus be considered valid. Using a required keyword parameter prevents that
|
115
|
+
# confusion between Ruby syntax and parameter name.
|
116
|
+
iseq = RubyVM::InstructionSequence.compile("def #{sentinel_method_name}(#{name}:); end", nil, nil, 0, false)
|
117
|
+
# pull out the first operation in the instruction sequence and its first argument and data
|
118
|
+
op, arg, data = iseq.to_a.dig(-1, 0)
|
119
|
+
# make sure that:
|
120
|
+
# 1. a method was defined, and
|
121
|
+
# 2. the method has the expected method name, and
|
122
|
+
# 3. the method has a keyword parameter with the expected name
|
123
|
+
op == :definemethod && arg == sentinel_method_name && data.dig(11, :keyword, 0) == name.to_sym
|
124
|
+
rescue SyntaxError
|
125
|
+
false
|
105
126
|
end
|
106
127
|
end
|
107
128
|
end
|
@@ -19,6 +19,11 @@ module Tapioca
|
|
19
19
|
::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(RUBY_VERSION))
|
20
20
|
end
|
21
21
|
|
22
|
+
sig { params(selector: String).returns(T::Boolean) }
|
23
|
+
def rails_version(selector)
|
24
|
+
::Gem::Requirement.new(selector).satisfied_by?(ActiveSupport.gem_version)
|
25
|
+
end
|
26
|
+
|
22
27
|
sig { params(src: String).returns(String) }
|
23
28
|
def template(src)
|
24
29
|
erb = if ERB_SUPPORTS_KVARGS
|
data/lib/tapioca/internal.rb
CHANGED
@@ -7,6 +7,7 @@ require "tapioca"
|
|
7
7
|
require "tapioca/runtime/reflection"
|
8
8
|
require "tapioca/runtime/trackers"
|
9
9
|
|
10
|
+
require "benchmark"
|
10
11
|
require "bundler"
|
11
12
|
require "erb"
|
12
13
|
require "etc"
|
@@ -33,6 +34,7 @@ require "tapioca/helpers/rbi_helper"
|
|
33
34
|
require "tapioca/sorbet_ext/fixed_hash_patch"
|
34
35
|
require "tapioca/sorbet_ext/name_patch"
|
35
36
|
require "tapioca/sorbet_ext/generic_name_patch"
|
37
|
+
require "tapioca/sorbet_ext/proc_bind_patch"
|
36
38
|
require "tapioca/runtime/generic_type_registry"
|
37
39
|
|
38
40
|
require "tapioca/helpers/cli_helper"
|
@@ -6,6 +6,7 @@ module Tapioca
|
|
6
6
|
class Loader
|
7
7
|
extend(T::Sig)
|
8
8
|
include Tapioca::GemHelper
|
9
|
+
include Thor::Base
|
9
10
|
|
10
11
|
sig do
|
11
12
|
params(gemfile: Tapioca::Gemfile, initialize_file: T.nilable(String), require_file: T.nilable(String)).void
|
@@ -29,12 +30,17 @@ module Tapioca
|
|
29
30
|
silence_deprecations
|
30
31
|
|
31
32
|
if environment_load
|
32
|
-
|
33
|
+
require "./config/environment"
|
33
34
|
else
|
34
|
-
|
35
|
+
require "./config/application"
|
35
36
|
end
|
36
37
|
|
37
38
|
eager_load_rails_app if eager_load
|
39
|
+
rescue LoadError, StandardError => e
|
40
|
+
say("Tapioca attempted to load the Rails application after encountering a `config/application.rb` file, " \
|
41
|
+
"but it failed. If your application uses Rails please ensure it can be loaded correctly before generating " \
|
42
|
+
"RBIs.\n#{e}", :yellow)
|
43
|
+
say("Continuing RBI generation without loading the Rails application.")
|
38
44
|
end
|
39
45
|
|
40
46
|
private
|
@@ -158,15 +158,14 @@ module Tapioca
|
|
158
158
|
# Examines the call stack to identify the closest location where a "require" is performed
|
159
159
|
# by searching for the label "<top (required)>". If none is found, it returns the location
|
160
160
|
# labeled "<main>", which is the original call site.
|
161
|
-
sig { returns(String) }
|
162
|
-
def
|
163
|
-
locations = Kernel.caller_locations
|
161
|
+
sig { params(locations: T.nilable(T::Array[Thread::Backtrace::Location])).returns(String) }
|
162
|
+
def resolve_loc(locations)
|
164
163
|
return "" unless locations
|
165
164
|
|
166
|
-
|
167
|
-
return "" unless
|
165
|
+
resolved_loc = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
|
166
|
+
return "" unless resolved_loc
|
168
167
|
|
169
|
-
|
168
|
+
resolved_loc.absolute_path || ""
|
170
169
|
end
|
171
170
|
|
172
171
|
sig { params(singleton_class: Module).returns(T.nilable(Module)) }
|
@@ -17,36 +17,54 @@ module Tapioca
|
|
17
17
|
const :path, String
|
18
18
|
end
|
19
19
|
|
20
|
-
@class_files = {}
|
20
|
+
@class_files = {}.compare_by_identity
|
21
21
|
|
22
22
|
# Immediately activated upon load. Observes class/module definition.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@class_files[key] ||= Set.new
|
39
|
-
@class_files[key] << ConstantLocation.new(path: T.must(file), lineno: T.must(lineno))
|
23
|
+
Tapioca.register_trace(:class) do |tp|
|
24
|
+
next if tp.self.singleton_class?
|
25
|
+
|
26
|
+
key = tp.self
|
27
|
+
|
28
|
+
path = tp.path
|
29
|
+
if File.exist?(path)
|
30
|
+
loc = build_constant_location(tp, caller_locations)
|
31
|
+
else
|
32
|
+
caller_location = T.must(caller_locations)
|
33
|
+
.find { |loc| loc.path && File.exist?(loc.path) }
|
34
|
+
|
35
|
+
next unless caller_location
|
36
|
+
|
37
|
+
loc = ConstantLocation.new(path: caller_location.absolute_path || "", lineno: caller_location.lineno)
|
40
38
|
end
|
39
|
+
|
40
|
+
(@class_files[key] ||= Set.new) << loc
|
41
|
+
end
|
42
|
+
|
43
|
+
Tapioca.register_trace(:c_return) do |tp|
|
44
|
+
next unless tp.method_id == :new
|
45
|
+
next unless Module === tp.return_value
|
46
|
+
|
47
|
+
key = tp.return_value
|
48
|
+
loc = build_constant_location(tp, caller_locations)
|
49
|
+
(@class_files[key] ||= Set.new) << loc
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.build_constant_location(tp, locations)
|
53
|
+
file = resolve_loc(caller_locations)
|
54
|
+
lineno = file == File.realpath(tp.path) ? tp.lineno : 0
|
55
|
+
|
56
|
+
ConstantLocation.new(path: file, lineno: lineno)
|
41
57
|
end
|
42
58
|
|
43
59
|
# Returns the files in which this class or module was opened. Doesn't know
|
44
60
|
# about situations where the class was opened prior to +require+ing,
|
45
61
|
# or where metaprogramming was used via +eval+, etc.
|
46
62
|
def self.files_for(klass)
|
47
|
-
|
48
|
-
|
49
|
-
|
63
|
+
locations_for(klass).map(&:path).to_set
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.locations_for(klass)
|
67
|
+
@class_files.fetch(klass, Set.new)
|
50
68
|
end
|
51
69
|
end
|
52
70
|
end
|
@@ -42,7 +42,7 @@ module Tapioca
|
|
42
42
|
def self.register(constant, mixin, mixin_type)
|
43
43
|
return unless @enabled
|
44
44
|
|
45
|
-
location = Reflection.
|
45
|
+
location = Reflection.resolve_loc(caller_locations)
|
46
46
|
|
47
47
|
constants = constants_with_mixin(mixin)
|
48
48
|
constants.fetch(mixin_type).store(constant, location)
|
@@ -1,18 +1,28 @@
|
|
1
1
|
# typed: true
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# We need sorbet to compile the signature for `qualified_name_of` before applying
|
5
|
-
# the patch to avoid an infinite loop.
|
6
|
-
T::Utils.signature_for_method(::Tapioca::Runtime::Reflection.method(:qualified_name_of))
|
7
|
-
|
8
4
|
module T
|
9
5
|
module Types
|
10
6
|
class Simple
|
11
7
|
module NamePatch
|
8
|
+
NAME_METHOD = T.let(Module.instance_method(:name), UnboundMethod)
|
9
|
+
|
12
10
|
def name
|
13
11
|
# Sorbet memoizes this method into the `@name` instance variable but
|
14
12
|
# doing so means that types get memoized before this patch is applied
|
15
|
-
|
13
|
+
qualified_name_of(@raw_type)
|
14
|
+
end
|
15
|
+
|
16
|
+
def qualified_name_of(constant)
|
17
|
+
name = NAME_METHOD.bind_call(constant)
|
18
|
+
name = nil if name&.start_with?("#<")
|
19
|
+
return if name.nil?
|
20
|
+
|
21
|
+
if name.start_with?("::")
|
22
|
+
name
|
23
|
+
else
|
24
|
+
"::#{name}"
|
25
|
+
end
|
16
26
|
end
|
17
27
|
end
|
18
28
|
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module T
|
5
|
+
module Types
|
6
|
+
module ProcBindPatch
|
7
|
+
def initialize(arg_types, returns, bind = T::Private::Methods::ARG_NOT_PROVIDED)
|
8
|
+
super(arg_types, returns)
|
9
|
+
|
10
|
+
unless bind == T::Private::Methods::ARG_NOT_PROVIDED
|
11
|
+
@bind = T.let(T::Utils.coerce(bind), T::Types::Base)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
name = super
|
17
|
+
name = name.sub("T.proc", "T.proc.bind(#{@bind})") unless @bind.nil?
|
18
|
+
name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Proc.prepend(ProcBindPatch)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module T
|
27
|
+
module Private
|
28
|
+
module Methods
|
29
|
+
module ProcBindPatch
|
30
|
+
def finalize_proc(decl)
|
31
|
+
super
|
32
|
+
|
33
|
+
T.unsafe(T::Types::Proc).new(decl.params, decl.returns, decl.bind)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
singleton_class.prepend(ProcBindPatch)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/tapioca/version.rb
CHANGED
data/lib/tapioca.rb
CHANGED
@@ -6,6 +6,18 @@ require "sorbet-runtime"
|
|
6
6
|
module Tapioca
|
7
7
|
extend T::Sig
|
8
8
|
|
9
|
+
@traces = T.let([], T::Array[TracePoint])
|
10
|
+
|
11
|
+
sig { params(trace_name: Symbol, block: T.proc.params(arg0: TracePoint).void).void }
|
12
|
+
def self.register_trace(trace_name, &block)
|
13
|
+
@traces << TracePoint.trace(trace_name, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { void }
|
17
|
+
def self.disable_traces
|
18
|
+
@traces.each(&:disable)
|
19
|
+
end
|
20
|
+
|
9
21
|
sig do
|
10
22
|
type_parameters(:Result)
|
11
23
|
.params(blk: T.proc.returns(T.type_parameter(:Result)))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tapioca
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ufuk Kayserilioglu
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2022-08-
|
14
|
+
date: 2022-08-22 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: bundler
|
@@ -224,6 +224,7 @@ files:
|
|
224
224
|
- lib/tapioca/gem/listeners/sorbet_required_ancestors.rb
|
225
225
|
- lib/tapioca/gem/listeners/sorbet_signatures.rb
|
226
226
|
- lib/tapioca/gem/listeners/sorbet_type_variables.rb
|
227
|
+
- lib/tapioca/gem/listeners/source_location.rb
|
227
228
|
- lib/tapioca/gem/listeners/subconstants.rb
|
228
229
|
- lib/tapioca/gem/listeners/yard_doc.rb
|
229
230
|
- lib/tapioca/gem/pipeline.rb
|
@@ -255,6 +256,7 @@ files:
|
|
255
256
|
- lib/tapioca/sorbet_ext/fixed_hash_patch.rb
|
256
257
|
- lib/tapioca/sorbet_ext/generic_name_patch.rb
|
257
258
|
- lib/tapioca/sorbet_ext/name_patch.rb
|
259
|
+
- lib/tapioca/sorbet_ext/proc_bind_patch.rb
|
258
260
|
- lib/tapioca/static/requires_compiler.rb
|
259
261
|
- lib/tapioca/static/symbol_loader.rb
|
260
262
|
- lib/tapioca/static/symbol_table_parser.rb
|