tapioca 0.8.3 → 0.9.2

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -2
  3. data/README.md +188 -36
  4. data/lib/tapioca/cli.rb +130 -66
  5. data/lib/tapioca/commands/annotations.rb +167 -34
  6. data/lib/tapioca/commands/check_shims.rb +101 -0
  7. data/lib/tapioca/commands/{init.rb → configure.rb} +1 -1
  8. data/lib/tapioca/commands/dsl.rb +1 -1
  9. data/lib/tapioca/commands/gem.rb +15 -10
  10. data/lib/tapioca/commands.rb +2 -1
  11. data/lib/tapioca/dsl/compiler.rb +1 -13
  12. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_relations.rb +17 -0
  14. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +5 -4
  15. data/lib/tapioca/dsl/compilers/frozen_record.rb +2 -2
  16. data/lib/tapioca/dsl/compilers/protobuf.rb +6 -0
  17. data/lib/tapioca/dsl/compilers.rb +0 -4
  18. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +21 -3
  19. data/lib/tapioca/dsl/pipeline.rb +0 -2
  20. data/lib/tapioca/dsl.rb +8 -0
  21. data/lib/tapioca/executor.rb +0 -3
  22. data/lib/tapioca/gem/events.rb +22 -3
  23. data/lib/tapioca/gem/listeners/base.rb +11 -0
  24. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +5 -0
  25. data/lib/tapioca/gem/listeners/foreign_constants.rb +65 -0
  26. data/lib/tapioca/gem/listeners/methods.rb +7 -18
  27. data/lib/tapioca/gem/listeners/mixins.rb +31 -10
  28. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +5 -0
  29. data/lib/tapioca/gem/listeners/sorbet_enums.rb +5 -0
  30. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +5 -0
  31. data/lib/tapioca/gem/listeners/sorbet_props.rb +5 -0
  32. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +5 -0
  33. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +6 -1
  34. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +5 -0
  35. data/lib/tapioca/gem/listeners/source_location.rb +67 -0
  36. data/lib/tapioca/gem/listeners/subconstants.rb +5 -0
  37. data/lib/tapioca/gem/listeners/yard_doc.rb +5 -0
  38. data/lib/tapioca/gem/listeners.rb +2 -0
  39. data/lib/tapioca/gem/pipeline.rb +64 -19
  40. data/lib/tapioca/gem.rb +6 -0
  41. data/lib/tapioca/gemfile.rb +7 -6
  42. data/lib/tapioca/helpers/cli_helper.rb +8 -2
  43. data/lib/tapioca/helpers/config_helper.rb +0 -2
  44. data/lib/tapioca/helpers/env_helper.rb +16 -0
  45. data/lib/tapioca/helpers/rbi_files_helper.rb +255 -0
  46. data/lib/tapioca/helpers/rbi_helper.rb +98 -94
  47. data/lib/tapioca/helpers/sorbet_helper.rb +2 -3
  48. data/lib/tapioca/helpers/test/content.rb +0 -2
  49. data/lib/tapioca/helpers/test/template.rb +0 -2
  50. data/lib/tapioca/internal.rb +36 -12
  51. data/lib/tapioca/rbi_ext/model.rb +2 -15
  52. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +18 -16
  53. data/lib/tapioca/runtime/reflection.rb +26 -0
  54. data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -16
  55. data/lib/tapioca/runtime/trackers/mixin.rb +49 -14
  56. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -4
  57. data/lib/tapioca/sorbet_ext/name_patch.rb +15 -5
  58. data/lib/tapioca/sorbet_ext/proc_bind_patch.rb +40 -0
  59. data/lib/tapioca/static/requires_compiler.rb +0 -2
  60. data/lib/tapioca/static/symbol_loader.rb +26 -30
  61. data/lib/tapioca/static/symbol_table_parser.rb +0 -3
  62. data/lib/tapioca/version.rb +1 -1
  63. data/lib/tapioca.rb +3 -0
  64. metadata +24 -7
  65. data/lib/tapioca/dsl/helpers/param_helper.rb +0 -55
  66. data/lib/tapioca/helpers/shims_helper.rb +0 -115
  67. data/lib/tapioca/helpers/signatures_helper.rb +0 -17
  68. data/lib/tapioca/helpers/type_variable_helper.rb +0 -43
@@ -27,6 +27,11 @@ module Tapioca
27
27
  end
28
28
  end
29
29
  end
30
+
31
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
32
+ def ignore?(event)
33
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
34
+ end
30
35
  end
31
36
  end
32
37
  end
@@ -18,6 +18,11 @@ module Tapioca
18
18
  event.node << RBI::RequiresAncestor.new(ancestor.to_s)
19
19
  end
20
20
  end
21
+
22
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
23
+ def ignore?(event)
24
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -8,7 +8,7 @@ module Tapioca
8
8
  extend T::Sig
9
9
 
10
10
  include Runtime::Reflection
11
- include SignaturesHelper
11
+ include RBIHelper
12
12
 
13
13
  TYPE_PARAMETER_MATCHER = /T\.type_parameter\(:?([[:word:]]+)\)/
14
14
 
@@ -73,6 +73,11 @@ module Tapioca
73
73
 
74
74
  final_methods.include?(signature.method_name)
75
75
  end
76
+
77
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
78
+ def ignore?(event)
79
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
80
+ end
76
81
  end
77
82
  end
78
83
  end
@@ -45,6 +45,11 @@ module Tapioca
45
45
 
46
46
  tree << RBI::Extend.new("T::Generic")
47
47
  end
48
+
49
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
50
+ def ignore?(event)
51
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
52
+ end
48
53
  end
49
54
  end
50
55
  end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Gem
6
+ module Listeners
7
+ class SourceLocation < Base
8
+ extend T::Sig
9
+
10
+ private
11
+
12
+ sig { override.params(event: ConstNodeAdded).void }
13
+ def on_const(event)
14
+ file, line = Object.const_source_location(event.symbol)
15
+ add_source_location_comment(event.node, file, line)
16
+ end
17
+
18
+ sig { override.params(event: ScopeNodeAdded).void }
19
+ def on_scope(event)
20
+ # Instead of using `const_source_location`, which always reports the first place where a constant is defined,
21
+ # we filter the locations tracked by ConstantDefinition. This allows us to provide the correct location for
22
+ # constants that are defined by multiple gems.
23
+ locations = Runtime::Trackers::ConstantDefinition.locations_for(event.constant)
24
+ location = locations.find do |loc|
25
+ Pathname.new(loc.path).realpath.to_s.include?(@pipeline.gem.full_gem_path)
26
+ end
27
+
28
+ # The location may still be nil in some situations, like constant aliases (e.g.: MyAlias = OtherConst). These
29
+ # are quite difficult to attribute a correct location, given that the source location points to the original
30
+ # constants and not the alias
31
+ add_source_location_comment(event.node, location.path, location.lineno) unless location.nil?
32
+ end
33
+
34
+ sig { override.params(event: MethodNodeAdded).void }
35
+ def on_method(event)
36
+ file, line = event.method.source_location
37
+ add_source_location_comment(event.node, file, line)
38
+ end
39
+
40
+ sig { params(node: RBI::NodeWithComments, file: T.nilable(String), line: T.nilable(Integer)).void }
41
+ def add_source_location_comment(node, file, line)
42
+ return unless file && line
43
+
44
+ gem = @pipeline.gem
45
+ path = Pathname.new(file)
46
+ return unless File.exist?(path)
47
+
48
+ # On native extensions, the source location may point to a shared object (.so, .bundle) file, which we cannot
49
+ # use for jump to definition. Only add source comments on Ruby files
50
+ return unless path.extname == ".rb"
51
+
52
+ path = if path.realpath.to_s.start_with?(gem.full_gem_path)
53
+ "#{gem.name}-#{gem.version}/#{path.realpath.relative_path_from(gem.full_gem_path)}"
54
+ else
55
+ path.sub("#{Bundler.bundle_path}/gems/", "").to_s
56
+ end
57
+
58
+ # Strip out the RUBY_ROOT prefix, which is different for each user
59
+ path = path.sub(RbConfig::CONFIG["rubylibdir"], "RUBY_ROOT")
60
+
61
+ node.comments << RBI::Comment.new("") if node.comments.any?
62
+ node.comments << RBI::Comment.new("source://#{path}:#{line}")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -31,6 +31,11 @@ module Tapioca
31
31
  @pipeline.push_constant(name, subconstant)
32
32
  end
33
33
  end
34
+
35
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
36
+ def ignore?(event)
37
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
38
+ end
34
39
  end
35
40
  end
36
41
  end
@@ -90,6 +90,11 @@ module Tapioca
90
90
 
91
91
  comments
92
92
  end
93
+
94
+ sig { override.params(event: NodeAdded).returns(T::Boolean) }
95
+ def ignore?(event)
96
+ event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
97
+ end
93
98
  end
94
99
  end
95
100
  end
@@ -13,4 +13,6 @@ require "tapioca/gem/listeners/sorbet_required_ancestors"
13
13
  require "tapioca/gem/listeners/sorbet_signatures"
14
14
  require "tapioca/gem/listeners/sorbet_type_variables"
15
15
  require "tapioca/gem/listeners/subconstants"
16
+ require "tapioca/gem/listeners/foreign_constants"
16
17
  require "tapioca/gem/listeners/yard_doc"
18
+ require "tapioca/gem/listeners/source_location"
@@ -1,22 +1,20 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "pathname"
5
-
6
4
  module Tapioca
7
5
  module Gem
8
6
  class Pipeline
9
7
  extend T::Sig
10
8
  include Runtime::Reflection
11
- include SignaturesHelper
9
+ include RBIHelper
12
10
 
13
11
  IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
14
12
 
15
13
  sig { returns(Gemfile::GemSpec) }
16
14
  attr_reader :gem
17
15
 
18
- sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean).void }
19
- 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)
20
18
  @root = T.let(RBI::Tree.new, RBI::Tree)
21
19
  @gem = gem
22
20
  @seen = T.let(Set.new, T::Set[String])
@@ -25,8 +23,8 @@ module Tapioca
25
23
  @events = T.let([], T::Array[Gem::Event])
26
24
 
27
25
  @payload_symbols = T.let(Static::SymbolLoader.payload_symbols, T::Set[String])
28
- @bootstrap_symbols = T.let(Static::SymbolLoader.gem_symbols(@gem).union(Static::SymbolLoader.engine_symbols),
29
- T::Set[String])
26
+ @bootstrap_symbols = T.let(load_bootstrap_symbols(@gem), T::Set[String])
27
+
30
28
  @bootstrap_symbols.each { |symbol| push_symbol(symbol) }
31
29
 
32
30
  @node_listeners = T.let([], T::Array[Gem::Listeners::Base])
@@ -41,6 +39,8 @@ module Tapioca
41
39
  @node_listeners << Gem::Listeners::SorbetSignatures.new(self)
42
40
  @node_listeners << Gem::Listeners::Subconstants.new(self)
43
41
  @node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
42
+ @node_listeners << Gem::Listeners::ForeignConstants.new(self)
43
+ @node_listeners << Gem::Listeners::SourceLocation.new(self) if include_loc
44
44
  @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
45
45
  end
46
46
 
@@ -60,27 +60,42 @@ module Tapioca
60
60
  @events << Gem::ConstantFound.new(symbol, constant)
61
61
  end
62
62
 
63
+ sig { params(symbol: String, constant: Module).void.checked(:never) }
64
+ def push_foreign_constant(symbol, constant)
65
+ @events << Gem::ForeignConstantFound.new(symbol, constant)
66
+ end
67
+
63
68
  sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
64
69
  def push_const(symbol, constant, node)
65
70
  @events << Gem::ConstNodeAdded.new(symbol, constant, node)
66
71
  end
67
72
 
68
- sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
73
+ sig do
74
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
75
+ end
69
76
  def push_scope(symbol, constant, node)
70
77
  @events << Gem::ScopeNodeAdded.new(symbol, constant, node)
71
78
  end
72
79
 
80
+ sig do
81
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
82
+ end
83
+ def push_foreign_scope(symbol, constant, node)
84
+ @events << Gem::ForeignScopeNodeAdded.new(symbol, constant, node)
85
+ end
86
+
73
87
  sig do
74
88
  params(
75
89
  symbol: String,
76
90
  constant: Module,
91
+ method: UnboundMethod,
77
92
  node: RBI::Method,
78
93
  signature: T.untyped,
79
94
  parameters: T::Array[[Symbol, String]]
80
95
  ).void.checked(:never)
81
96
  end
82
- def push_method(symbol, constant, node, signature, parameters)
83
- @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)
84
99
  end
85
100
 
86
101
  sig { params(symbol_name: String).returns(T::Boolean) }
@@ -114,6 +129,14 @@ module Tapioca
114
129
 
115
130
  private
116
131
 
132
+ sig { params(gem: Gemfile::GemSpec).returns(T::Set[String]) }
133
+ def load_bootstrap_symbols(gem)
134
+ engine_symbols = Static::SymbolLoader.engine_symbols(gem)
135
+ gem_symbols = Static::SymbolLoader.gem_symbols(gem)
136
+
137
+ gem_symbols.union(engine_symbols)
138
+ end
139
+
117
140
  sig { returns(Gem::Event) }
118
141
  def next_event
119
142
  T.must(@events.shift)
@@ -150,13 +173,14 @@ module Tapioca
150
173
  return if name.start_with?("#<")
151
174
  return if name.downcase == name
152
175
  return if alias_namespaced?(name)
153
- return if seen?(name)
154
176
 
155
- constant = event.constant
156
- return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
177
+ return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`
157
178
 
158
- mark_seen(name)
159
- compile_constant(name, constant)
179
+ if event.is_a?(Gem::ForeignConstantFound)
180
+ compile_foreign_constant(name, event.constant)
181
+ else
182
+ compile_constant(name, event.constant)
183
+ end
160
184
  end
161
185
 
162
186
  sig { params(event: Gem::NodeAdded).void }
@@ -166,6 +190,11 @@ module Tapioca
166
190
 
167
191
  # Compile
168
192
 
193
+ sig { params(symbol: String, constant: Module).void }
194
+ def compile_foreign_constant(symbol, constant)
195
+ compile_module(symbol, constant, foreign_constant: true)
196
+ end
197
+
169
198
  sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
170
199
  def compile_constant(symbol, constant)
171
200
  case constant
@@ -182,6 +211,10 @@ module Tapioca
182
211
 
183
212
  sig { params(name: String, constant: Module).void }
184
213
  def compile_alias(name, constant)
214
+ return if seen?(name)
215
+
216
+ mark_seen(name)
217
+
185
218
  return if symbol_in_payload?(name)
186
219
 
187
220
  target = name_of(constant)
@@ -199,6 +232,10 @@ module Tapioca
199
232
 
200
233
  sig { params(name: String, value: BasicObject).void.checked(:never) }
201
234
  def compile_object(name, value)
235
+ return if seen?(name)
236
+
237
+ mark_seen(name)
238
+
202
239
  return if symbol_in_payload?(name)
203
240
 
204
241
  klass = class_of(value)
@@ -228,10 +265,13 @@ module Tapioca
228
265
  @root << node
229
266
  end
230
267
 
231
- sig { params(name: String, constant: Module).void }
232
- def compile_module(name, constant)
233
- return unless defined_in_gem?(constant, strict: false)
268
+ sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void }
269
+ def compile_module(name, constant, foreign_constant: false)
270
+ return unless defined_in_gem?(constant, strict: false) || foreign_constant
234
271
  return if Tapioca::TypeVariableModule === constant
272
+ return if seen?(name)
273
+
274
+ mark_seen(name)
235
275
 
236
276
  scope =
237
277
  if constant.is_a?(Class)
@@ -241,7 +281,12 @@ module Tapioca
241
281
  RBI::Module.new(name)
242
282
  end
243
283
 
244
- push_scope(name, constant, scope)
284
+ if foreign_constant
285
+ push_foreign_scope(name, constant, scope)
286
+ else
287
+ push_scope(name, constant, scope)
288
+ end
289
+
245
290
  @root << scope
246
291
  end
247
292
 
@@ -0,0 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/gem/events"
5
+ require "tapioca/gem/listeners"
6
+ require "tapioca/gem/pipeline"
@@ -1,10 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler"
5
- require "logger"
6
- require "yard-sorbet"
7
-
8
4
  module Tapioca
9
5
  class Gemfile
10
6
  extend(T::Sig)
@@ -140,8 +136,13 @@ module Tapioca
140
136
  extend(T::Sig)
141
137
  include GemHelper
142
138
 
143
- IGNORED_GEMS = T.let(["sorbet", "sorbet-static", "sorbet-runtime", "sorbet-static-and-runtime"].freeze,
144
- T::Array[String])
139
+ IGNORED_GEMS = T.let(
140
+ [
141
+ "sorbet", "sorbet-static", "sorbet-runtime", "sorbet-static-and-runtime",
142
+ "debug", "fakefs",
143
+ ].freeze,
144
+ T::Array[String]
145
+ )
145
146
 
146
147
  sig { returns(String) }
147
148
  attr_reader :full_gem_path, :version
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "thor"
5
-
6
4
  module Tapioca
7
5
  module CliHelper
8
6
  extend T::Sig
@@ -30,5 +28,13 @@ module Tapioca
30
28
  rbi_formatter.max_line_length = options[:rbi_max_line_length]
31
29
  rbi_formatter
32
30
  end
31
+
32
+ sig { params(options: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
33
+ def netrc_file(options)
34
+ return nil if options[:auth]
35
+ return nil unless options[:netrc]
36
+
37
+ options[:netrc_file] || ENV["TAPIOCA_NETRC_FILE"] || File.join(ENV["HOME"].to_s, ".netrc")
38
+ end
33
39
  end
34
40
  end
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "yaml"
5
-
6
4
  module Tapioca
7
5
  module ConfigHelper
8
6
  extend T::Sig
@@ -0,0 +1,16 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module EnvHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor }
10
+
11
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
12
+ def set_environment(options) # rubocop:disable Naming/AccessorMethodName
13
+ ENV["RAILS_ENV"] = ENV["RACK_ENV"] = options[:environment]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,255 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module RBIFilesHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor::Shell }
10
+ requires_ancestor { SorbetHelper }
11
+
12
+ sig { params(index: RBI::Index, kind: String, file: String).void }
13
+ def index_rbi(index, kind, file)
14
+ return unless File.exist?(file)
15
+
16
+ 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)")
22
+ end
23
+
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:)
26
+ return unless Dir.exist?(dir) && !Dir.empty?(dir)
27
+
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
+ end
40
+
41
+ sig do
42
+ params(
43
+ index: RBI::Index,
44
+ shim_rbi_dir: String,
45
+ todo_rbi_file: String
46
+ ).returns(T::Hash[String, T::Array[RBI::Node]])
47
+ end
48
+ def duplicated_nodes_from_index(index, shim_rbi_dir:, todo_rbi_file:)
49
+ duplicates = {}
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)
55
+
56
+ duplicates[key] = nodes
57
+ end
58
+ end
59
+ say(" Done ", :green)
60
+ say("(#{time.round(2)}s)")
61
+ duplicates
62
+ end
63
+
64
+ sig { params(loc: RBI::Loc, path_prefix: T.nilable(String)).returns(String) }
65
+ def location_to_payload_url(loc, path_prefix:)
66
+ return loc.to_s unless path_prefix
67
+
68
+ url = loc.file || ""
69
+ return loc.to_s unless url.start_with?(path_prefix)
70
+
71
+ url = url.sub(path_prefix, SorbetHelper::SORBET_PAYLOAD_URL)
72
+ url = "#{url}#L#{loc.begin_line}"
73
+ url
74
+ end
75
+
76
+ sig do
77
+ params(
78
+ command: String,
79
+ gem_dir: String,
80
+ dsl_dir: String,
81
+ auto_strictness: T::Boolean,
82
+ gems: T::Array[Gemfile::GemSpec],
83
+ compilers: T::Enumerable[Class]
84
+ ).void
85
+ end
86
+ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
87
+ error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
88
+
89
+ say("Checking generated RBI files... ")
90
+ res = sorbet(
91
+ "--no-config",
92
+ "--error-url-base=#{error_url_base}",
93
+ "--stop-after namer",
94
+ dsl_dir,
95
+ gem_dir
96
+ )
97
+ say(" Done", :green)
98
+
99
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
100
+
101
+ if errors.empty?
102
+ say(" No errors found\n\n", [:green, :bold])
103
+ return
104
+ end
105
+
106
+ parse_errors = errors.select { |error| error.code < 4000 }
107
+
108
+ if parse_errors.any?
109
+ say_error(<<~ERR, :red)
110
+
111
+ ##### INTERNAL ERROR #####
112
+
113
+ There are parse errors in the generated RBI files.
114
+
115
+ This seems related to a bug in Tapioca.
116
+ Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
117
+
118
+ Tapioca v#{Tapioca::VERSION}
119
+
120
+ Command:
121
+ #{command}
122
+
123
+ ERR
124
+
125
+ say_error(<<~ERR, :red) if gems.any?
126
+ Gems:
127
+ #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
128
+
129
+ ERR
130
+
131
+ say_error(<<~ERR, :red) if compilers.any?
132
+ Compilers:
133
+ #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
134
+
135
+ ERR
136
+
137
+ say_error(<<~ERR, :red)
138
+ Errors:
139
+ #{parse_errors.map { |error| " #{error}" }.join("\n")}
140
+
141
+ ##########################
142
+
143
+ ERR
144
+ end
145
+
146
+ if auto_strictness
147
+ redef_errors = errors.select { |error| error.code == 4010 }
148
+ update_gem_rbis_strictnesses(redef_errors, gem_dir)
149
+ end
150
+
151
+ Kernel.exit(1) if parse_errors.any?
152
+ end
153
+
154
+ private
155
+
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)
166
+ end
167
+
168
+ index.visit_all(trees)
169
+ end
170
+
171
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
172
+ def shims_or_todos_have_duplicates?(nodes, shim_rbi_dir:, todo_rbi_file:)
173
+ return false if nodes.size == 1
174
+
175
+ shims_or_todos = extract_shims_and_todos(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
176
+ return false if shims_or_todos.empty?
177
+
178
+ shims_or_todos_empty_scopes = extract_empty_scopes(shims_or_todos)
179
+ return true unless shims_or_todos_empty_scopes.empty?
180
+
181
+ props = extract_methods_and_attrs(shims_or_todos)
182
+ return false if props.empty?
183
+
184
+ shims_or_todos_with_sigs = extract_nodes_with_sigs(props)
185
+ shims_or_todos_with_sigs.each do |shim_or_todo|
186
+ shims_or_todos_sigs = shim_or_todo.sigs
187
+
188
+ extract_methods_and_attrs(nodes).each do |node|
189
+ next if node == shim_or_todo
190
+ return true if shims_or_todos_sigs.all? { |sig| node.sigs.include?(sig) }
191
+ end
192
+
193
+ return false
194
+ end
195
+
196
+ true
197
+ end
198
+
199
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Array[RBI::Node]) }
200
+ def extract_shims_and_todos(nodes, shim_rbi_dir:, todo_rbi_file:)
201
+ nodes.select do |node|
202
+ node.loc&.file&.start_with?(shim_rbi_dir) || node.loc&.file == todo_rbi_file
203
+ end
204
+ end
205
+
206
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[RBI::Scope]) }
207
+ def extract_empty_scopes(nodes)
208
+ T.cast(nodes.select { |node| node.is_a?(RBI::Scope) && node.empty? }, T::Array[RBI::Scope])
209
+ end
210
+
211
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
212
+ def extract_methods_and_attrs(nodes)
213
+ T.cast(nodes.select do |node|
214
+ node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
215
+ end, T::Array[T.any(RBI::Method, RBI::Attr)])
216
+ end
217
+
218
+ sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
219
+ def extract_nodes_with_sigs(nodes)
220
+ nodes.reject { |node| node.sigs.empty? }
221
+ end
222
+
223
+ sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
224
+ def update_gem_rbis_strictnesses(errors, gem_dir)
225
+ files = []
226
+
227
+ errors.each do |error|
228
+ # Collect the file with error
229
+ files << error.file
230
+ error.more.each do |line|
231
+ # Also collect the conflicting definition file paths
232
+ next unless line.include?("Previous definition")
233
+
234
+ files << line.split(":").first&.strip
235
+ end
236
+ end
237
+
238
+ files
239
+ .uniq
240
+ .sort
241
+ .select { |file| file.start_with?(gem_dir) }
242
+ .each do |file|
243
+ Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
244
+ say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
245
+ end
246
+
247
+ say("\n")
248
+ end
249
+
250
+ sig { params(path: String).returns(String) }
251
+ def gem_name_from_rbi_path(path)
252
+ T.must(File.basename(path, ".rbi").split("@").first)
253
+ end
254
+ end
255
+ end