tapioca 0.9.0 → 0.9.1

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: 590bba5b82c68fea817819ca7d749a8527cc74915df1989639d6b9a23792f4ac
4
- data.tar.gz: 1209c829aca470f50a86db985e3764827c679fca90c312f39daac81b3389d196
3
+ metadata.gz: e53d805bebee7cb1898eb795daebc5c7f586b5dea934a6056dfc2e1037168e83
4
+ data.tar.gz: a9baac8cc53cc87f24495961dfe738603900e9818542c722b79211da2aaea84a
5
5
  SHA512:
6
- metadata.gz: 151fb7e27e03ffaab3a9f7723333154bff8b773d5d98c24e048c726966a2e8c37ea59781d9fc2d445e2b0dd98bbb932461ad525aeb262b97ff56468da693e515
7
- data.tar.gz: 81e6a1f2f25ccd489ae50076a3b07082e53cf279375d5dbd5219916591a3c49b743bc022aea97cef5ff7f88fe919e2d5d1a632892a8596fcb5e888032096e7de
6
+ metadata.gz: 101e3405eb9fbc011e3ddb8ad33a622c74f18cde1d3b6be204aa7840fc7d1f89b1c61e61585cfe9d26c397e67839d09fcd85671c5c0b92f2c8b78741ee4e5ded
7
+ data.tar.gz: 0a7ad144e5860f72158c194de01213809a5294b69eb2a6de12288d827ca574efdc8937c967ec5c578b1a7c429d4f9e29bd5a1ee6738a373cf4cc57ca2ed8163a
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,6 +275,7 @@ 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
274
280
  command = Commands::CheckShims.new(
275
281
  gem_rbi_dir: options[:gem_rbi_dir],
@@ -277,7 +283,8 @@ module Tapioca
277
283
  shim_rbi_dir: options[:shim_rbi_dir],
278
284
  annotations_rbi_dir: options[:annotations_rbi_dir],
279
285
  todo_rbi_file: options[:todo_rbi_file],
280
- payload: options[:payload]
286
+ payload: options[:payload],
287
+ number_of_workers: options[:workers]
281
288
  )
282
289
  command.execute
283
290
  end
@@ -193,7 +193,8 @@ module Tapioca
193
193
  # Please run `#{default_command(:annotations)}` to update it.
194
194
  COMMENT
195
195
 
196
- contents = content.split("\n")
196
+ # Split contents into newlines and ensure trailing empty lines are included
197
+ contents = content.split("\n", -1)
197
198
  if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
198
199
  contents.insert(2, header).join("\n")
199
200
  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 }
@@ -51,7 +61,7 @@ module Tapioca
51
61
  exit(1)
52
62
  end
53
63
 
54
- index_payload(index, payload_path)
64
+ index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
55
65
  end
56
66
  else
57
67
  say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
@@ -62,10 +72,10 @@ module Tapioca
62
72
  end
63
73
 
64
74
  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)
75
+ index_rbis(index, "shim", @shim_rbi_dir, number_of_workers: @number_of_workers)
76
+ index_rbis(index, "gem", @gem_rbi_dir, number_of_workers: @number_of_workers)
77
+ index_rbis(index, "dsl", @dsl_rbi_dir, number_of_workers: @number_of_workers)
78
+ index_rbis(index, "annotation", @annotations_rbi_dir, number_of_workers: @number_of_workers)
69
79
 
70
80
  duplicates = duplicated_nodes_from_index(index, shim_rbi_dir: @shim_rbi_dir, todo_rbi_file: @todo_rbi_file)
71
81
  unless duplicates.empty?
@@ -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
 
@@ -182,7 +185,7 @@ module Tapioca
182
185
  default_command(:gem, gem.name),
183
186
  reason: "types exported from the `#{gem.name}` gem",) if @file_header
184
187
 
185
- rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc).compile
188
+ rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc, include_loc: @include_loc).compile
186
189
 
187
190
  merge_with_exported_rbi(gem, rbi) if @include_exported_rbis
188
191
 
@@ -201,11 +204,13 @@ module Tapioca
201
204
  end
202
205
  end
203
206
 
204
- sig { void }
205
- def perform_sync_verification
207
+ sig { params(exclude: T::Array[String]).void }
208
+ def perform_sync_verification(exclude: [])
206
209
  diff = {}
207
210
 
208
211
  removed_rbis.each do |gem_name|
212
+ next if exclude.include?(gem_name)
213
+
209
214
  filename = existing_rbi(gem_name)
210
215
  diff[filename] = :removed
211
216
  end
@@ -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"
@@ -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,16 @@ 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
+ case column_type.subtype
123
+ when ActiveRecord::Type::Integer
124
+ "T.any(::String, ::Symbol, ::Integer)"
125
+ else
126
+ "T.any(::String, ::Symbol)"
127
+ end
128
+ end
111
129
  end
112
130
  end
113
131
  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,60 @@
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
+ path = if path.realpath.to_s.start_with?(gem.full_gem_path)
49
+ "#{gem.name}-#{gem.version}/#{path.realpath.relative_path_from(gem.full_gem_path)}"
50
+ else
51
+ path.sub("#{Bundler.bundle_path}/gems/", "")
52
+ end
53
+
54
+ node.comments << RBI::Comment.new("") if node.comments.any?
55
+ node.comments << RBI::Comment.new("source://#{path}:#{line}")
56
+ end
57
+ end
58
+ end
59
+ end
60
+ 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) }
@@ -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
 
@@ -150,19 +153,19 @@ module Tapioca
150
153
 
151
154
  private
152
155
 
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)
156
+ sig { params(index: RBI::Index, files: T::Array[String], number_of_workers: T.nilable(Integer)).void }
157
+ def parse_and_index_files(index, files, number_of_workers:)
158
+ executor = Executor.new(files, number_of_workers: number_of_workers)
159
+
160
+ trees = executor.run_in_parallel do |file|
161
+ next if Spoom::Sorbet::Sigils.file_strictness(file) == "ignore"
162
+
163
+ RBI::Parser.parse_file(file)
164
+ rescue RBI::ParseError => e
165
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
157
166
  end
158
- end
159
167
 
160
- sig { params(index: RBI::Index, file: String).void }
161
- def parse_and_index_file(index, file)
162
- tree = RBI::Parser.parse_file(file)
163
- index.visit(tree)
164
- rescue RBI::ParseError => e
165
- say_error("\nWarning: #{e} (#{e.location})", :yellow)
168
+ index.visit_all(trees)
166
169
  end
167
170
 
168
171
  sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
@@ -93,7 +93,10 @@ module Tapioca
93
93
 
94
94
  sig { params(name: String).returns(T::Boolean) }
95
95
  def valid_method_name?(name)
96
- !name.to_sym.inspect.start_with?(':"', ":@", ":$")
96
+ name == "==" || !(
97
+ name.to_sym.inspect.start_with?(':"', ":@", ":$") ||
98
+ name.delete_suffix("=").to_sym.inspect.start_with?(':"', ":@", ":$")
99
+ )
97
100
  end
98
101
 
99
102
  sig { params(name: String).returns(T::Boolean) }
@@ -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"
@@ -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(constant: Module).returns(T.nilable(String)) }
@@ -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
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))
24
+ next if tp.self.singleton_class?
25
+
26
+ key = tp.self
27
+
28
+ if tp.path == "(eval)"
29
+ caller_location = T.must(caller_locations)
30
+ .drop_while { |loc| loc.path == "(eval)" }
31
+ .first
32
+
33
+ next unless caller_location
34
+
35
+ loc = ConstantLocation.new(path: caller_location.absolute_path || "", lineno: caller_location.lineno)
36
+ else
37
+ loc = build_constant_location(tp, caller_locations)
40
38
  end
39
+
40
+ (@class_files[key] ||= Set.new) << loc
41
+ end
42
+
43
+ TracePoint.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
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.9.0"
5
+ VERSION = "0.9.1"
6
6
  end
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.0
4
+ version: 0.9.1
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-07-07 00:00:00.000000000 Z
14
+ date: 2022-07-14 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