tapioca 0.9.2 → 0.9.3

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