tapioca 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92e743973e21c5d863f898255a4ccfcf620deea13cbb4807d43ad0c58085285e
4
- data.tar.gz: 4a8e05c974c11d4d201e1f2342e1769260a63ceaff18d54a71167dd929f6f23a
3
+ metadata.gz: 212af3a2ba5204b2aa479e52acddf879f32755faaee95691ce51e5c26b306481
4
+ data.tar.gz: e8f957c96874e08fed7cce1580ac110c41bef0853c6f52f2e46c90bb6435bfe8
5
5
  SHA512:
6
- metadata.gz: 92b8a9e0306b22869147b1f0a3912f6fb88e3583e54caab6dbc37450996d15046ae049bea5fcfd46090575e94c7b4b16ff4ca833b63fdc125b30601211a2603e
7
- data.tar.gz: d76e8b15e040f6fe60cb940ef3654dffd45eaf3edaaa563a6404a178014bc767866c5668d0dc2560610e75adb5034ac6ae0c94d19d6fa1c5d4812c5769a78a74
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
- say_error("Options `--no-netrc` and `--netrc-file` can't be used together", :bold, :red)
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
- say_error("\nCan't fetch annotations without sources (no index fetched)", :bold, :red)
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 { |uri, index| T.must(hash[gem_name]) << uri if index.has_gem?(gem_name) }
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
- exit(0)
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 = content.split("\n")
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(gem_rbi_dir:, dsl_rbi_dir:, annotations_rbi_dir:, shim_rbi_dir:, todo_rbi_file:, payload:)
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
- exit(0)
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
- say_error("Sorbet failed to dump payload")
50
- say_error(result.err)
51
- exit(1)
60
+ raise Thor::Error, <<~ERROR
61
+ "Sorbet failed to dump payload"
62
+ #{result.err}
63
+ ERROR
52
64
  end
53
65
 
54
- index_payload(index, payload_path)
66
+ index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
55
67
  end
56
68
  else
57
- say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
58
- say_error("Current: v#{SORBET_GEM_SPEC.version}")
59
- say_error("Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}")
60
- exit(1)
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
- say_error("\nDuplicated RBI for #{key}:", :red)
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
- say_error(" * #{loc_string}", :red)
97
+ messages << set_color(" * #{loc_string}", :red)
80
98
  end
81
99
  end
82
- say_error("\nPlease remove the duplicated definitions from #{@shim_rbi_dir} and #{@todo_rbi_file}", :red)
83
- exit(1)
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
@@ -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
- exit(1)
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.each do |name, _|
216
- say("Error: Cannot find compiler '#{name}'", :red)
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
- exit(1)
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
- say("RBI files are out-of-date. In your development environment, please run:", :green)
336
- say(" `#{default_command(command)}`", [:green, :bold])
337
- say("Once it is complete, be sure to commit and push any changes", :green)
338
-
339
- say("")
340
-
341
- say("Reason:", [:red])
342
- diff.group_by(&:last).sort.each do |cause, diff_for_cause|
343
- say(build_error_for_files(cause, diff_for_cause.map(&:first)))
344
- end
345
-
346
- exit(1)
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
 
@@ -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
- say("Error: Cannot find gem '#{gem_name}'", :red)
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
- say("RBI files are out-of-date. In your development environment, please run:", :green)
329
- say(" `#{default_command(command)}`", [:green, :bold])
330
- say("Once it is complete, be sure to commit and push any changes", :green)
331
-
332
- say("")
333
-
334
- say("Reason:", [:red])
335
- diff.group_by(&:last).sort.each do |cause, diff_for_cause|
336
- say(build_error_for_files(cause, diff_for_cause.map(&:first)))
337
- end
338
-
339
- exit(1)
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.ancestors # get all ancestors from class that includes AR fixtures
46
- .drop(1) # drop the anonymous class itself from the array
47
- .reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
48
- .map! do |mod|
49
- [mod.private_instance_methods(false), mod.instance_methods(false)]
50
- end
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 = T.unsafe(constant).__tapioca_scope_names
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 = getter_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
@@ -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
@@ -15,3 +15,4 @@ require "tapioca/gem/listeners/sorbet_type_variables"
15
15
  require "tapioca/gem/listeners/subconstants"
16
16
  require "tapioca/gem/listeners/foreign_constants"
17
17
  require "tapioca/gem/listeners/yard_doc"
18
+ require "tapioca/gem/listeners/source_location"
@@ -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.candidates.map(&:file).to_a.compact
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
- print_errors(config_file, errors)
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]).void }
177
- def print_errors(config_file, errors)
178
- say_error("\nConfiguration file ", :red)
179
- say_error("#{config_file} ", :blue, :bold)
180
- say_error("has the following errors:\n\n", :red)
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
- errors.each do |error|
183
- say_error("- ")
184
- error.message_parts.each do |part|
185
- T.unsafe(self).say_error(part.message, *part.colors)
186
- end
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
- parse_and_index_file(index, file)
28
- say(" Done", :green)
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
- say("Loading #{kind} RBIs from #{dir}... ")
36
- files = Dir.glob("#{dir}/**/*.rbi").sort
37
- parse_and_index_files(index, files)
38
- say(" Done", :green)
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
- index.keys.each do |key|
52
- nodes = index[key]
53
- next unless shims_or_todos_have_duplicates?(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
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
- duplicates[key] = nodes
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
- if parse_errors.any?
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
- say_error(<<~ERR, :red) if gems.any?
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
- say_error(<<~ERR, :red) if compilers.any?
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
- say_error(<<~ERR, :red)
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.exit(1) if parse_errors.any?
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.each do |file|
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
- sig { params(index: RBI::Index, file: String).void }
161
- def parse_and_index_file(index, file)
162
- return if Spoom::Sorbet::Sigils.file_strictness(file) == "ignore"
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
- tree = RBI::Parser.parse_file(file)
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
- name == "==" || !(
97
- name.to_sym.inspect.start_with?(':"', ":@", ":$") ||
98
- name.delete_suffix("=").to_sym.inspect.start_with?(':"', ":@", ":$")
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
- /^([[:lower:]]|_|[^[[:ascii:]]])([[:alnum:]]|_|[^[[:ascii:]]])*$/.match?(name)
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
@@ -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
- safe_require("./config/environment")
33
+ require "./config/environment"
33
34
  else
34
- safe_require("./config/application")
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 required_from_location
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
- required_location = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
167
- return "" unless required_location
165
+ resolved_loc = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
166
+ return "" unless resolved_loc
168
167
 
169
- required_location.absolute_path || ""
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
- TracePoint.trace(:class) do |tp|
24
- unless tp.self.singleton_class?
25
- key = name_of(tp.self)
26
- file = tp.path
27
- lineno = tp.lineno
28
-
29
- if file == "(eval)"
30
- caller_location = T.must(caller_locations)
31
- .drop_while { |loc| loc.path == "(eval)" }
32
- .first
33
-
34
- file = caller_location&.path
35
- lineno = caller_location&.lineno
36
- end
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
- name = String === klass ? klass : name_of(klass)
48
- files = @class_files.fetch(name, [])
49
- files.map(&:path).to_set
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.required_from_location
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
- ::Tapioca::Runtime::Reflection.qualified_name_of(@raw_type)
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.9.3"
5
+ VERSION = "0.9.4"
6
6
  end
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.3
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-19 00:00:00.000000000 Z
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