tapioca 0.8.3 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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