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 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