tapioca 0.9.2 → 0.9.3

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: c9cda50b7996366aafe00f6b8643f17385088e842c9626c23b922202702d8ee4
4
- data.tar.gz: 040d9732ca74f020d475c3965392b63c2b9a525876cfd765b2c21fa0d031df54
3
+ metadata.gz: 92e743973e21c5d863f898255a4ccfcf620deea13cbb4807d43ad0c58085285e
4
+ data.tar.gz: 4a8e05c974c11d4d201e1f2342e1769260a63ceaff18d54a71167dd929f6f23a
5
5
  SHA512:
6
- metadata.gz: a7678c337e77326cedb49b06088ec1b24ae9b08f2047f02efc741134dc6a6efe851a702f45e02c571850c1762aca4de134067015aa8f2da06be9aa1cabb2050b
7
- data.tar.gz: edd3d2148f8c4ee3c04bdc5025502e859f8d726bf38c741d4357a152770608ba639ad420070a8e071a261b4459bb20b7b637a390991877fd757e9d04a3d0d4b7
6
+ metadata.gz: 92b8a9e0306b22869147b1f0a3912f6fb88e3583e54caab6dbc37450996d15046ae049bea5fcfd46090575e94c7b4b16ff4ca833b63fdc125b30601211a2603e
7
+ data.tar.gz: d76e8b15e040f6fe60cb940ef3654dffd45eaf3edaaa563a6404a178014bc767866c5668d0dc2560610e75adb5034ac6ae0c94d19d6fa1c5d4812c5769a78a74
data/README.md CHANGED
@@ -152,8 +152,6 @@ 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
157
155
  [--exported-gem-rbis], [--no-exported-gem-rbis] # Include RBIs found in the `rbi/` directory of the gem
158
156
  # Default: true
159
157
  -w, [--workers=N] # EXPERIMENTAL: Number of parallel workers to use when generating RBIs
@@ -742,8 +740,6 @@ Options:
742
740
  # Default: sorbet/rbi/todo.rbi
743
741
  [--payload], [--no-payload] # Check shims against Sorbet's payload
744
742
  # Default: true
745
- -w, [--workers=N] # EXPERIMENTAL: Number of parallel workers
746
- # Default: 1
747
743
  -c, [--config=<config file path>] # Path to the Tapioca configuration file
748
744
  # Default: sorbet/tapioca/config.yml
749
745
  -V, [--verbose], [--no-verbose] # Verbose output for debugging purposes
@@ -806,7 +802,6 @@ gem:
806
802
  activesupport: 'false'
807
803
  verify: false
808
804
  doc: true
809
- loc: true
810
805
  exported_gem_rbis: true
811
806
  workers: 1
812
807
  auto_strictness: true
@@ -820,7 +815,6 @@ check_shims:
820
815
  annotations_rbi_dir: sorbet/rbi/annotations
821
816
  todo_rbi_file: sorbet/rbi/todo.rbi
822
817
  payload: true
823
- workers: 1
824
818
  annotations:
825
819
  sources:
826
820
  - https://raw.githubusercontent.com/Shopify/rbi-central/main
data/lib/tapioca/cli.rb CHANGED
@@ -190,10 +190,6 @@ 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
197
193
  option :exported_gem_rbis,
198
194
  type: :boolean,
199
195
  desc: "Include RBIs found in the `rbi/` directory of the gem",
@@ -237,7 +233,6 @@ module Tapioca
237
233
  outpath: Pathname.new(options[:outdir]),
238
234
  file_header: options[:file_header],
239
235
  include_doc: options[:doc],
240
- include_loc: options[:loc],
241
236
  include_exported_rbis: options[:exported_gem_rbis],
242
237
  number_of_workers: options[:workers],
243
238
  auto_strictness: options[:auto_strictness],
@@ -260,7 +255,7 @@ module Tapioca
260
255
  end
261
256
 
262
257
  if gems.empty? && !all
263
- command.sync(should_verify: verify, exclude: options[:exclude])
258
+ command.sync(should_verify: verify)
264
259
  else
265
260
  command.execute
266
261
  end
@@ -275,7 +270,6 @@ module Tapioca
275
270
  option :annotations_rbi_dir, type: :string, desc: "Path to annotations RBIs", default: DEFAULT_ANNOTATIONS_DIR
276
271
  option :todo_rbi_file, type: :string, desc: "Path to the generated todo RBI file", default: DEFAULT_TODO_FILE
277
272
  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
279
273
  def check_shims
280
274
  command = Commands::CheckShims.new(
281
275
  gem_rbi_dir: options[:gem_rbi_dir],
@@ -283,8 +277,7 @@ module Tapioca
283
277
  shim_rbi_dir: options[:shim_rbi_dir],
284
278
  annotations_rbi_dir: options[:annotations_rbi_dir],
285
279
  todo_rbi_file: options[:todo_rbi_file],
286
- payload: options[:payload],
287
- number_of_workers: options[:workers]
280
+ payload: options[:payload]
288
281
  )
289
282
  command.execute
290
283
  end
@@ -112,7 +112,7 @@ module Tapioca
112
112
  fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
113
113
 
114
114
  gem_names.each_with_object(fetchable_gems) do |gem_name, hash|
115
- @indexes.each { |uri, index| hash[gem_name] << uri if index.has_gem?(gem_name) }
115
+ @indexes.each { |uri, index| T.must(hash[gem_name]) << uri if index.has_gem?(gem_name) }
116
116
  end
117
117
 
118
118
  if fetchable_gems.empty?
@@ -193,8 +193,7 @@ module Tapioca
193
193
  # Please run `#{default_command(:annotations)}` to update it.
194
194
  COMMENT
195
195
 
196
- # Split contents into newlines and ensure trailing empty lines are included
197
- contents = content.split("\n", -1)
196
+ contents = content.split("\n")
198
197
  if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
199
198
  contents.insert(2, header).join("\n")
200
199
  else
@@ -15,19 +15,10 @@ module Tapioca
15
15
  annotations_rbi_dir: String,
16
16
  shim_rbi_dir: String,
17
17
  todo_rbi_file: String,
18
- payload: T::Boolean,
19
- number_of_workers: T.nilable(Integer)
18
+ payload: T::Boolean
20
19
  ).void
21
20
  end
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
- )
21
+ def initialize(gem_rbi_dir:, dsl_rbi_dir:, annotations_rbi_dir:, shim_rbi_dir:, todo_rbi_file:, payload:)
31
22
  super()
32
23
  @gem_rbi_dir = gem_rbi_dir
33
24
  @dsl_rbi_dir = dsl_rbi_dir
@@ -35,7 +26,6 @@ module Tapioca
35
26
  @shim_rbi_dir = shim_rbi_dir
36
27
  @todo_rbi_file = todo_rbi_file
37
28
  @payload = payload
38
- @number_of_workers = number_of_workers
39
29
  end
40
30
 
41
31
  sig { override.void }
@@ -61,7 +51,7 @@ module Tapioca
61
51
  exit(1)
62
52
  end
63
53
 
64
- index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
54
+ index_payload(index, payload_path)
65
55
  end
66
56
  else
67
57
  say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
@@ -72,10 +62,10 @@ module Tapioca
72
62
  end
73
63
 
74
64
  index_rbi(index, "todo", @todo_rbi_file)
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)
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)
79
69
 
80
70
  duplicates = duplicated_nodes_from_index(index, shim_rbi_dir: @shim_rbi_dir, todo_rbi_file: @todo_rbi_file)
81
71
  unless duplicates.empty?
@@ -17,7 +17,6 @@ module Tapioca
17
17
  outpath: Pathname,
18
18
  file_header: T::Boolean,
19
19
  include_doc: T::Boolean,
20
- include_loc: T::Boolean,
21
20
  include_exported_rbis: T::Boolean,
22
21
  number_of_workers: T.nilable(Integer),
23
22
  auto_strictness: T::Boolean,
@@ -34,7 +33,6 @@ module Tapioca
34
33
  outpath:,
35
34
  file_header:,
36
35
  include_doc:,
37
- include_loc:,
38
36
  include_exported_rbis:,
39
37
  number_of_workers: nil,
40
38
  auto_strictness: true,
@@ -60,7 +58,6 @@ module Tapioca
60
58
  @existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
61
59
  @expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
62
60
  @include_doc = T.let(include_doc, T::Boolean)
63
- @include_loc = T.let(include_loc, T::Boolean)
64
61
  @include_exported_rbis = include_exported_rbis
65
62
  end
66
63
 
@@ -97,12 +94,12 @@ module Tapioca
97
94
  end
98
95
  end
99
96
 
100
- sig { params(should_verify: T::Boolean, exclude: T::Array[String]).void }
101
- def sync(should_verify: false, exclude: [])
97
+ sig { params(should_verify: T::Boolean).void }
98
+ def sync(should_verify: false)
102
99
  if should_verify
103
100
  say("Checking for out-of-date RBIs...")
104
101
  say("")
105
- perform_sync_verification(exclude: exclude)
102
+ perform_sync_verification
106
103
  return
107
104
  end
108
105
 
@@ -185,7 +182,7 @@ module Tapioca
185
182
  default_command(:gem, gem.name),
186
183
  reason: "types exported from the `#{gem.name}` gem",) if @file_header
187
184
 
188
- rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc, include_loc: @include_loc).compile
185
+ rbi.root = Tapioca::Gem::Pipeline.new(gem, include_doc: @include_doc).compile
189
186
 
190
187
  merge_with_exported_rbi(gem, rbi) if @include_exported_rbis
191
188
 
@@ -204,13 +201,11 @@ module Tapioca
204
201
  end
205
202
  end
206
203
 
207
- sig { params(exclude: T::Array[String]).void }
208
- def perform_sync_verification(exclude: [])
204
+ sig { void }
205
+ def perform_sync_verification
209
206
  diff = {}
210
207
 
211
208
  removed_rbis.each do |gem_name|
212
- next if exclude.include?(gem_name)
213
-
214
209
  filename = existing_rbi(gem_name)
215
210
  diff[filename] = :removed
216
211
  end
@@ -52,7 +52,6 @@ module Tapioca
52
52
  T::Set[Module].new(gather_constants).compare_by_identity,
53
53
  T.nilable(T::Set[Module])
54
54
  )
55
- T.must(@processable_constants)
56
55
  end
57
56
 
58
57
  # NOTE: This should eventually accept an `Error` object or `Exception` rather than simply a `String`.
@@ -216,7 +216,6 @@ module Tapioca
216
216
  sig { returns(String) }
217
217
  def constant_name
218
218
  @constant_name ||= T.let(T.must(qualified_name_of(constant)), T.nilable(String))
219
- T.must(@constant_name)
220
219
  end
221
220
 
222
221
  sig { params(method_name: Symbol).returns(T::Boolean) }
@@ -65,7 +65,7 @@ module Tapioca
65
65
  class FrozenRecord < Compiler
66
66
  extend T::Sig
67
67
 
68
- ConstantType = type_member { { fixed: T.all(T.class_of(::FrozenRecord::Base), Extensions::FrozenRecord) } }
68
+ ConstantType = type_member { { fixed: T.class_of(::FrozenRecord::Base) } }
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 = constant.__tapioca_scope_names
100
+ scopes = T.unsafe(constant).__tapioca_scope_names
101
101
  return if scopes.nil?
102
102
 
103
103
  module_name = "GeneratedRelationMethods"
@@ -38,20 +38,12 @@ module Tapioca
38
38
  "::Time"
39
39
  when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
40
40
  "::ActiveSupport::TimeWithZone"
41
- when ActiveRecord::Enum::EnumType
42
- "::String"
43
41
  else
44
42
  handle_unknown_type(column_type)
45
43
  end
46
44
 
47
45
  column = @constant.columns_hash[column_name]
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
46
+ setter_type = getter_type
55
47
 
56
48
  if column&.null
57
49
  return [as_nilable_type(getter_type), as_nilable_type(setter_type)]
@@ -116,16 +108,6 @@ module Tapioca
116
108
 
117
109
  first_argument_type.to_s
118
110
  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
129
111
  end
130
112
  end
131
113
  end
@@ -105,9 +105,6 @@ module Tapioca
105
105
  class MethodNodeAdded < NodeAdded
106
106
  extend T::Sig
107
107
 
108
- sig { returns(UnboundMethod) }
109
- attr_reader :method
110
-
111
108
  sig { returns(RBI::Method) }
112
109
  attr_reader :node
113
110
 
@@ -121,16 +118,14 @@ module Tapioca
121
118
  params(
122
119
  symbol: String,
123
120
  constant: Module,
124
- method: UnboundMethod,
125
121
  node: RBI::Method,
126
122
  signature: T.untyped,
127
123
  parameters: T::Array[[Symbol, String]]
128
124
  ).void.checked(:never)
129
125
  end
130
- def initialize(symbol, constant, method, node, signature, parameters) # rubocop:disable Metrics/ParameterLists
126
+ def initialize(symbol, constant, node, signature, parameters)
131
127
  super(symbol, constant)
132
128
  @node = node
133
- @method = method
134
129
  @signature = signature
135
130
  @parameters = parameters
136
131
  end
@@ -35,10 +35,10 @@ module Tapioca
35
35
  # base constant. Then, generate RBIs as if the base constant is extending the mixin,
36
36
  # which is functionally equivalent to including or prepending to the singleton class.
37
37
  if !name && constant.singleton_class?
38
- name = constant_name_from_singleton_class(constant)
39
- next unless name
38
+ attached_class = attached_class_of(constant)
39
+ next unless attached_class
40
40
 
41
- constant = T.cast(constantize(name), Module)
41
+ constant = attached_class
42
42
  end
43
43
 
44
44
  @pipeline.push_foreign_constant(name, constant) if name
@@ -134,7 +134,7 @@ module Tapioca
134
134
  end
135
135
  end
136
136
 
137
- @pipeline.push_method(symbol_name, constant, method, rbi_method, signature, sanitized_parameters)
137
+ @pipeline.push_method(symbol_name, constant, rbi_method, signature, sanitized_parameters)
138
138
  tree << rbi_method
139
139
  end
140
140
 
@@ -15,4 +15,3 @@ 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, include_loc: T::Boolean).void }
17
- def initialize(gem, include_doc: false, include_loc: false)
16
+ sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean).void }
17
+ def initialize(gem, include_doc: 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,7 +40,6 @@ 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
44
43
  @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
45
44
  end
46
45
 
@@ -88,14 +87,13 @@ module Tapioca
88
87
  params(
89
88
  symbol: String,
90
89
  constant: Module,
91
- method: UnboundMethod,
92
90
  node: RBI::Method,
93
91
  signature: T.untyped,
94
92
  parameters: T::Array[[Symbol, String]]
95
93
  ).void.checked(:never)
96
94
  end
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)
95
+ def push_method(symbol, constant, node, signature, parameters)
96
+ @events << Gem::MethodNodeAdded.new(symbol, constant, node, signature, parameters)
99
97
  end
100
98
 
101
99
  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
+
12
22
  sig { params(index: RBI::Index, kind: String, file: String).void }
13
23
  def index_rbi(index, kind, file)
14
24
  return unless File.exist?(file)
15
25
 
16
26
  say("Loading #{kind} RBIs from #{file}... ")
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)")
27
+ parse_and_index_file(index, file)
28
+ say(" Done", :green)
22
29
  end
23
30
 
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:)
31
+ sig { params(index: RBI::Index, kind: String, dir: String).void }
32
+ def index_rbis(index, kind, dir)
26
33
  return unless Dir.exist?(dir) && !Dir.empty?(dir)
27
34
 
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)")
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)
39
39
  end
40
40
 
41
41
  sig do
@@ -48,16 +48,13 @@ 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
- 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)
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)
55
54
 
56
- duplicates[key] = nodes
57
- end
55
+ duplicates[key] = nodes
58
56
  end
59
- say(" Done ", :green)
60
- say("(#{time.round(2)}s)")
57
+ say(" Done", :green)
61
58
  duplicates
62
59
  end
63
60
 
@@ -153,19 +150,21 @@ module Tapioca
153
150
 
154
151
  private
155
152
 
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)
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)
166
157
  end
158
+ end
159
+
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"
167
163
 
168
- index.visit_all(trees)
164
+ tree = RBI::Parser.parse_file(file)
165
+ index.visit(tree)
166
+ rescue RBI::ParseError => e
167
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
169
168
  end
170
169
 
171
170
  sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
@@ -178,6 +177,9 @@ module Tapioca
178
177
  shims_or_todos_empty_scopes = extract_empty_scopes(shims_or_todos)
179
178
  return true unless shims_or_todos_empty_scopes.empty?
180
179
 
180
+ mixins = extract_mixins(shims_or_todos)
181
+ return true unless mixins.empty?
182
+
181
183
  props = extract_methods_and_attrs(shims_or_todos)
182
184
  return false if props.empty?
183
185
 
@@ -215,6 +217,13 @@ module Tapioca
215
217
  end, T::Array[T.any(RBI::Method, RBI::Attr)])
216
218
  end
217
219
 
220
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Mixin, RBI::RequiresAncestor)]) }
221
+ def extract_mixins(nodes)
222
+ T.cast(nodes.select do |node|
223
+ node.is_a?(RBI::Mixin) || node.is_a?(RBI::RequiresAncestor)
224
+ end, T::Array[T.all(RBI::Mixin, RBI::RequiresAncestor)])
225
+ end
226
+
218
227
  sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
219
228
  def extract_nodes_with_sigs(nodes)
220
229
  nodes.reject { |node| node.sigs.empty? }
@@ -93,36 +93,15 @@ module Tapioca
93
93
 
94
94
  sig { params(name: String).returns(T::Boolean) }
95
95
  def valid_method_name?(name)
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
96
+ name == "==" || !(
97
+ name.to_sym.inspect.start_with?(':"', ":@", ":$") ||
98
+ name.delete_suffix("=").to_sym.inspect.start_with?(':"', ":@", ":$")
99
+ )
106
100
  end
107
101
 
108
102
  sig { params(name: String).returns(T::Boolean) }
109
103
  def valid_parameter_name?(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
104
+ /^([[:lower:]]|_|[^[[:ascii:]]])([[:alnum:]]|_|[^[[:ascii:]]])*$/.match?(name)
126
105
  end
127
106
  end
128
107
  end
@@ -7,7 +7,6 @@ require "tapioca"
7
7
  require "tapioca/runtime/reflection"
8
8
  require "tapioca/runtime/trackers"
9
9
 
10
- require "benchmark"
11
10
  require "bundler"
12
11
  require "erb"
13
12
  require "etc"
@@ -34,7 +33,6 @@ require "tapioca/helpers/rbi_helper"
34
33
  require "tapioca/sorbet_ext/fixed_hash_patch"
35
34
  require "tapioca/sorbet_ext/name_patch"
36
35
  require "tapioca/sorbet_ext/generic_name_patch"
37
- require "tapioca/sorbet_ext/proc_bind_patch"
38
36
  require "tapioca/runtime/generic_type_registry"
39
37
 
40
38
  require "tapioca/helpers/cli_helper"
@@ -101,7 +101,7 @@ module RBI
101
101
 
102
102
  sig { returns(T::Hash[String, RBI::Node]) }
103
103
  def nodes_cache
104
- T.must(@nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node])))
104
+ @nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node]))
105
105
  end
106
106
 
107
107
  sig { params(node: RBI::Node).returns(RBI::Node) }
@@ -22,7 +22,7 @@ module Tapioca
22
22
  def write_empty_body_comment!(file)
23
23
  file.comments << RBI::BlankLine.new unless file.comments.empty?
24
24
  file.comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
25
- file.comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
25
+ file.comments << RBI::Comment.new("see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem")
26
26
  end
27
27
  end
28
28
 
@@ -158,25 +158,25 @@ 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 { params(locations: T.nilable(T::Array[Thread::Backtrace::Location])).returns(String) }
162
- def resolve_loc(locations)
161
+ sig { returns(String) }
162
+ def required_from_location
163
+ locations = Kernel.caller_locations
163
164
  return "" unless locations
164
165
 
165
- resolved_loc = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
166
- return "" unless resolved_loc
166
+ required_location = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
167
+ return "" unless required_location
167
168
 
168
- resolved_loc.absolute_path || ""
169
+ required_location.absolute_path || ""
169
170
  end
170
171
 
171
- sig { params(constant: Module).returns(T.nilable(String)) }
172
- def constant_name_from_singleton_class(constant)
173
- constant.to_s.match("#<Class:(.+)>")&.captures&.first
174
- end
172
+ sig { params(singleton_class: Module).returns(T.nilable(Module)) }
173
+ def attached_class_of(singleton_class)
174
+ # https://stackoverflow.com/a/36622320/98634
175
+ result = ObjectSpace.each_object(singleton_class).find do |klass|
176
+ singleton_class_of(T.cast(klass, Module)) == singleton_class
177
+ end
175
178
 
176
- sig { params(constant: Module).returns(T.nilable(BasicObject)) }
177
- def constant_from_singleton_class(constant)
178
- constant_name = constant_name_from_singleton_class(constant)
179
- constantize(constant_name) if constant_name
179
+ T.cast(result, Module)
180
180
  end
181
181
  end
182
182
  end
@@ -17,54 +17,36 @@ module Tapioca
17
17
  const :path, String
18
18
  end
19
19
 
20
- @class_files = {}.compare_by_identity
20
+ @class_files = {}
21
21
 
22
22
  # Immediately activated upon load. Observes class/module definition.
23
23
  TracePoint.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)
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))
38
40
  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)
57
41
  end
58
42
 
59
43
  # Returns the files in which this class or module was opened. Doesn't know
60
44
  # about situations where the class was opened prior to +require+ing,
61
45
  # or where metaprogramming was used via +eval+, etc.
62
46
  def self.files_for(klass)
63
- locations_for(klass).map(&:path).to_set
64
- end
65
-
66
- def self.locations_for(klass)
67
- @class_files.fetch(klass, Set.new)
47
+ name = String === klass ? klass : name_of(klass)
48
+ files = @class_files.fetch(name, [])
49
+ files.map(&:path).to_set
68
50
  end
69
51
  end
70
52
  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.resolve_loc(caller_locations)
45
+ location = Reflection.required_from_location
46
46
 
47
47
  constants = constants_with_mixin(mixin)
48
48
  constants.fetch(mixin_type).store(constant, location)
@@ -103,7 +103,7 @@ class Module
103
103
  # this mixin can be found whether searching for an include/prepend on the singleton class
104
104
  # or an extend on the attached class.
105
105
  def register_extend_on_attached_class(constant)
106
- attached_class = Tapioca::Runtime::Reflection.constant_from_singleton_class(constant)
106
+ attached_class = Tapioca::Runtime::Reflection.attached_class_of(constant)
107
107
 
108
108
  Tapioca::Runtime::Trackers::Mixin.register(
109
109
  T.cast(attached_class, Module),
@@ -1,28 +1,18 @@
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
+
4
8
  module T
5
9
  module Types
6
10
  class Simple
7
11
  module NamePatch
8
- NAME_METHOD = T.let(Module.instance_method(:name), UnboundMethod)
9
-
10
12
  def name
11
13
  # Sorbet memoizes this method into the `@name` instance variable but
12
14
  # doing so means that types get memoized before this patch is applied
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
15
+ ::Tapioca::Runtime::Reflection.qualified_name_of(@raw_type)
26
16
  end
27
17
  end
28
18
 
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.9.2"
5
+ VERSION = "0.9.3"
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.2
4
+ version: 0.9.3
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-19 00:00:00.000000000 Z
14
+ date: 2022-08-19 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
@@ -224,7 +224,6 @@ 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
228
227
  - lib/tapioca/gem/listeners/subconstants.rb
229
228
  - lib/tapioca/gem/listeners/yard_doc.rb
230
229
  - lib/tapioca/gem/pipeline.rb
@@ -256,7 +255,6 @@ files:
256
255
  - lib/tapioca/sorbet_ext/fixed_hash_patch.rb
257
256
  - lib/tapioca/sorbet_ext/generic_name_patch.rb
258
257
  - lib/tapioca/sorbet_ext/name_patch.rb
259
- - lib/tapioca/sorbet_ext/proc_bind_patch.rb
260
258
  - lib/tapioca/static/requires_compiler.rb
261
259
  - lib/tapioca/static/symbol_loader.rb
262
260
  - lib/tapioca/static/symbol_table_parser.rb
@@ -1,67 +0,0 @@
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
@@ -1,40 +0,0 @@
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