tapioca 0.8.3 → 0.9.0

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -2
  3. data/README.md +182 -36
  4. data/lib/tapioca/cli.rb +122 -65
  5. data/lib/tapioca/commands/annotations.rb +165 -33
  6. data/lib/tapioca/commands/check_shims.rb +91 -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 +5 -5
  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/protobuf.rb +6 -0
  16. data/lib/tapioca/dsl/compilers.rb +0 -4
  17. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +2 -2
  18. data/lib/tapioca/dsl/pipeline.rb +0 -2
  19. data/lib/tapioca/dsl.rb +8 -0
  20. data/lib/tapioca/executor.rb +0 -3
  21. data/lib/tapioca/gem/events.rb +16 -2
  22. data/lib/tapioca/gem/listeners/base.rb +11 -0
  23. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +5 -0
  24. data/lib/tapioca/gem/listeners/foreign_constants.rb +65 -0
  25. data/lib/tapioca/gem/listeners/methods.rb +6 -17
  26. data/lib/tapioca/gem/listeners/mixins.rb +31 -10
  27. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +5 -0
  28. data/lib/tapioca/gem/listeners/sorbet_enums.rb +5 -0
  29. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +5 -0
  30. data/lib/tapioca/gem/listeners/sorbet_props.rb +5 -0
  31. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +5 -0
  32. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +6 -1
  33. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +5 -0
  34. data/lib/tapioca/gem/listeners/subconstants.rb +5 -0
  35. data/lib/tapioca/gem/listeners/yard_doc.rb +5 -0
  36. data/lib/tapioca/gem/listeners.rb +1 -0
  37. data/lib/tapioca/gem/pipeline.rb +58 -15
  38. data/lib/tapioca/gem.rb +6 -0
  39. data/lib/tapioca/gemfile.rb +7 -6
  40. data/lib/tapioca/helpers/cli_helper.rb +8 -2
  41. data/lib/tapioca/helpers/config_helper.rb +0 -2
  42. data/lib/tapioca/helpers/env_helper.rb +16 -0
  43. data/lib/tapioca/helpers/rbi_files_helper.rb +252 -0
  44. data/lib/tapioca/helpers/rbi_helper.rb +74 -94
  45. data/lib/tapioca/helpers/sorbet_helper.rb +2 -3
  46. data/lib/tapioca/helpers/test/content.rb +0 -2
  47. data/lib/tapioca/helpers/test/template.rb +0 -2
  48. data/lib/tapioca/internal.rb +34 -12
  49. data/lib/tapioca/rbi_ext/model.rb +2 -15
  50. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +18 -16
  51. data/lib/tapioca/runtime/reflection.rb +27 -0
  52. data/lib/tapioca/runtime/trackers/constant_definition.rb +17 -7
  53. data/lib/tapioca/runtime/trackers/mixin.rb +49 -14
  54. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -4
  55. data/lib/tapioca/static/requires_compiler.rb +0 -2
  56. data/lib/tapioca/static/symbol_loader.rb +26 -30
  57. data/lib/tapioca/static/symbol_table_parser.rb +0 -3
  58. data/lib/tapioca/version.rb +1 -1
  59. data/lib/tapioca.rb +3 -0
  60. metadata +22 -7
  61. data/lib/tapioca/dsl/helpers/param_helper.rb +0 -55
  62. data/lib/tapioca/helpers/shims_helper.rb +0 -115
  63. data/lib/tapioca/helpers/signatures_helper.rb +0 -17
  64. data/lib/tapioca/helpers/type_variable_helper.rb +0 -43
@@ -1,14 +1,12 @@
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
 
@@ -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,7 @@ 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)
44
43
  @node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
45
44
  end
46
45
 
@@ -60,16 +59,30 @@ module Tapioca
60
59
  @events << Gem::ConstantFound.new(symbol, constant)
61
60
  end
62
61
 
62
+ sig { params(symbol: String, constant: Module).void.checked(:never) }
63
+ def push_foreign_constant(symbol, constant)
64
+ @events << Gem::ForeignConstantFound.new(symbol, constant)
65
+ end
66
+
63
67
  sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
64
68
  def push_const(symbol, constant, node)
65
69
  @events << Gem::ConstNodeAdded.new(symbol, constant, node)
66
70
  end
67
71
 
68
- sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
72
+ sig do
73
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
74
+ end
69
75
  def push_scope(symbol, constant, node)
70
76
  @events << Gem::ScopeNodeAdded.new(symbol, constant, node)
71
77
  end
72
78
 
79
+ sig do
80
+ params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never)
81
+ end
82
+ def push_foreign_scope(symbol, constant, node)
83
+ @events << Gem::ForeignScopeNodeAdded.new(symbol, constant, node)
84
+ end
85
+
73
86
  sig do
74
87
  params(
75
88
  symbol: String,
@@ -114,6 +127,14 @@ module Tapioca
114
127
 
115
128
  private
116
129
 
130
+ sig { params(gem: Gemfile::GemSpec).returns(T::Set[String]) }
131
+ def load_bootstrap_symbols(gem)
132
+ engine_symbols = Static::SymbolLoader.engine_symbols(gem)
133
+ gem_symbols = Static::SymbolLoader.gem_symbols(gem)
134
+
135
+ gem_symbols.union(engine_symbols)
136
+ end
137
+
117
138
  sig { returns(Gem::Event) }
118
139
  def next_event
119
140
  T.must(@events.shift)
@@ -150,13 +171,14 @@ module Tapioca
150
171
  return if name.start_with?("#<")
151
172
  return if name.downcase == name
152
173
  return if alias_namespaced?(name)
153
- return if seen?(name)
154
174
 
155
- constant = event.constant
156
- return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
175
+ return if T::Enum === event.constant # T::Enum instances are defined via `compile_enums`
157
176
 
158
- mark_seen(name)
159
- compile_constant(name, constant)
177
+ if event.is_a?(Gem::ForeignConstantFound)
178
+ compile_foreign_constant(name, event.constant)
179
+ else
180
+ compile_constant(name, event.constant)
181
+ end
160
182
  end
161
183
 
162
184
  sig { params(event: Gem::NodeAdded).void }
@@ -166,6 +188,11 @@ module Tapioca
166
188
 
167
189
  # Compile
168
190
 
191
+ sig { params(symbol: String, constant: Module).void }
192
+ def compile_foreign_constant(symbol, constant)
193
+ compile_module(symbol, constant, foreign_constant: true)
194
+ end
195
+
169
196
  sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
170
197
  def compile_constant(symbol, constant)
171
198
  case constant
@@ -182,6 +209,10 @@ module Tapioca
182
209
 
183
210
  sig { params(name: String, constant: Module).void }
184
211
  def compile_alias(name, constant)
212
+ return if seen?(name)
213
+
214
+ mark_seen(name)
215
+
185
216
  return if symbol_in_payload?(name)
186
217
 
187
218
  target = name_of(constant)
@@ -199,6 +230,10 @@ module Tapioca
199
230
 
200
231
  sig { params(name: String, value: BasicObject).void.checked(:never) }
201
232
  def compile_object(name, value)
233
+ return if seen?(name)
234
+
235
+ mark_seen(name)
236
+
202
237
  return if symbol_in_payload?(name)
203
238
 
204
239
  klass = class_of(value)
@@ -228,10 +263,13 @@ module Tapioca
228
263
  @root << node
229
264
  end
230
265
 
231
- sig { params(name: String, constant: Module).void }
232
- def compile_module(name, constant)
233
- return unless defined_in_gem?(constant, strict: false)
266
+ sig { params(name: String, constant: Module, foreign_constant: T::Boolean).void }
267
+ def compile_module(name, constant, foreign_constant: false)
268
+ return unless defined_in_gem?(constant, strict: false) || foreign_constant
234
269
  return if Tapioca::TypeVariableModule === constant
270
+ return if seen?(name)
271
+
272
+ mark_seen(name)
235
273
 
236
274
  scope =
237
275
  if constant.is_a?(Class)
@@ -241,7 +279,12 @@ module Tapioca
241
279
  RBI::Module.new(name)
242
280
  end
243
281
 
244
- push_scope(name, constant, scope)
282
+ if foreign_constant
283
+ push_foreign_scope(name, constant, scope)
284
+ else
285
+ push_scope(name, constant, scope)
286
+ end
287
+
245
288
  @root << scope
246
289
  end
247
290
 
@@ -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,252 @@
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, dir: String).void }
13
+ def index_payload(index, dir)
14
+ return unless Dir.exist?(dir)
15
+
16
+ say("Loading Sorbet payload... ")
17
+ files = Dir.glob("#{dir}/**/*.rbi").sort
18
+ parse_and_index_files(index, files)
19
+ say(" Done", :green)
20
+ end
21
+
22
+ sig { params(index: RBI::Index, kind: String, file: String).void }
23
+ def index_rbi(index, kind, file)
24
+ return unless File.exist?(file)
25
+
26
+ say("Loading #{kind} RBIs from #{file}... ")
27
+ parse_and_index_file(index, file)
28
+ say(" Done", :green)
29
+ end
30
+
31
+ sig { params(index: RBI::Index, kind: String, dir: String).void }
32
+ def index_rbis(index, kind, dir)
33
+ return unless Dir.exist?(dir) && !Dir.empty?(dir)
34
+
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
+ 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
+ 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)
54
+
55
+ duplicates[key] = nodes
56
+ end
57
+ say(" Done", :green)
58
+ duplicates
59
+ end
60
+
61
+ sig { params(loc: RBI::Loc, path_prefix: T.nilable(String)).returns(String) }
62
+ def location_to_payload_url(loc, path_prefix:)
63
+ return loc.to_s unless path_prefix
64
+
65
+ url = loc.file || ""
66
+ return loc.to_s unless url.start_with?(path_prefix)
67
+
68
+ url = url.sub(path_prefix, SorbetHelper::SORBET_PAYLOAD_URL)
69
+ url = "#{url}#L#{loc.begin_line}"
70
+ url
71
+ end
72
+
73
+ sig do
74
+ params(
75
+ command: String,
76
+ gem_dir: String,
77
+ dsl_dir: String,
78
+ auto_strictness: T::Boolean,
79
+ gems: T::Array[Gemfile::GemSpec],
80
+ compilers: T::Enumerable[Class]
81
+ ).void
82
+ end
83
+ def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
84
+ error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
85
+
86
+ say("Checking generated RBI files... ")
87
+ res = sorbet(
88
+ "--no-config",
89
+ "--error-url-base=#{error_url_base}",
90
+ "--stop-after namer",
91
+ dsl_dir,
92
+ gem_dir
93
+ )
94
+ say(" Done", :green)
95
+
96
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
97
+
98
+ if errors.empty?
99
+ say(" No errors found\n\n", [:green, :bold])
100
+ return
101
+ end
102
+
103
+ parse_errors = errors.select { |error| error.code < 4000 }
104
+
105
+ if parse_errors.any?
106
+ say_error(<<~ERR, :red)
107
+
108
+ ##### INTERNAL ERROR #####
109
+
110
+ There are parse errors in the generated RBI files.
111
+
112
+ This seems related to a bug in Tapioca.
113
+ Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
114
+
115
+ Tapioca v#{Tapioca::VERSION}
116
+
117
+ Command:
118
+ #{command}
119
+
120
+ ERR
121
+
122
+ say_error(<<~ERR, :red) if gems.any?
123
+ Gems:
124
+ #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
125
+
126
+ ERR
127
+
128
+ say_error(<<~ERR, :red) if compilers.any?
129
+ Compilers:
130
+ #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
131
+
132
+ ERR
133
+
134
+ say_error(<<~ERR, :red)
135
+ Errors:
136
+ #{parse_errors.map { |error| " #{error}" }.join("\n")}
137
+
138
+ ##########################
139
+
140
+ ERR
141
+ end
142
+
143
+ if auto_strictness
144
+ redef_errors = errors.select { |error| error.code == 4010 }
145
+ update_gem_rbis_strictnesses(redef_errors, gem_dir)
146
+ end
147
+
148
+ Kernel.exit(1) if parse_errors.any?
149
+ end
150
+
151
+ private
152
+
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)
157
+ end
158
+ end
159
+
160
+ sig { params(index: RBI::Index, file: String).void }
161
+ def parse_and_index_file(index, file)
162
+ tree = RBI::Parser.parse_file(file)
163
+ index.visit(tree)
164
+ rescue RBI::ParseError => e
165
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
166
+ end
167
+
168
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Boolean) }
169
+ def shims_or_todos_have_duplicates?(nodes, shim_rbi_dir:, todo_rbi_file:)
170
+ return false if nodes.size == 1
171
+
172
+ shims_or_todos = extract_shims_and_todos(nodes, shim_rbi_dir: shim_rbi_dir, todo_rbi_file: todo_rbi_file)
173
+ return false if shims_or_todos.empty?
174
+
175
+ shims_or_todos_empty_scopes = extract_empty_scopes(shims_or_todos)
176
+ return true unless shims_or_todos_empty_scopes.empty?
177
+
178
+ props = extract_methods_and_attrs(shims_or_todos)
179
+ return false if props.empty?
180
+
181
+ shims_or_todos_with_sigs = extract_nodes_with_sigs(props)
182
+ shims_or_todos_with_sigs.each do |shim_or_todo|
183
+ shims_or_todos_sigs = shim_or_todo.sigs
184
+
185
+ extract_methods_and_attrs(nodes).each do |node|
186
+ next if node == shim_or_todo
187
+ return true if shims_or_todos_sigs.all? { |sig| node.sigs.include?(sig) }
188
+ end
189
+
190
+ return false
191
+ end
192
+
193
+ true
194
+ end
195
+
196
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String, todo_rbi_file: String).returns(T::Array[RBI::Node]) }
197
+ def extract_shims_and_todos(nodes, shim_rbi_dir:, todo_rbi_file:)
198
+ nodes.select do |node|
199
+ node.loc&.file&.start_with?(shim_rbi_dir) || node.loc&.file == todo_rbi_file
200
+ end
201
+ end
202
+
203
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[RBI::Scope]) }
204
+ def extract_empty_scopes(nodes)
205
+ T.cast(nodes.select { |node| node.is_a?(RBI::Scope) && node.empty? }, T::Array[RBI::Scope])
206
+ end
207
+
208
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
209
+ def extract_methods_and_attrs(nodes)
210
+ T.cast(nodes.select do |node|
211
+ node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
212
+ end, T::Array[T.any(RBI::Method, RBI::Attr)])
213
+ end
214
+
215
+ sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
216
+ def extract_nodes_with_sigs(nodes)
217
+ nodes.reject { |node| node.sigs.empty? }
218
+ end
219
+
220
+ sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
221
+ def update_gem_rbis_strictnesses(errors, gem_dir)
222
+ files = []
223
+
224
+ errors.each do |error|
225
+ # Collect the file with error
226
+ files << error.file
227
+ error.more.each do |line|
228
+ # Also collect the conflicting definition file paths
229
+ next unless line.include?("Previous definition")
230
+
231
+ files << line.split(":").first&.strip
232
+ end
233
+ end
234
+
235
+ files
236
+ .uniq
237
+ .sort
238
+ .select { |file| file.start_with?(gem_dir) }
239
+ .each do |file|
240
+ Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
241
+ say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
242
+ end
243
+
244
+ say("\n")
245
+ end
246
+
247
+ sig { params(path: String).returns(String) }
248
+ def gem_name_from_rbi_path(path)
249
+ T.must(File.basename(path, ".rbi").split("@").first)
250
+ end
251
+ end
252
+ end
@@ -4,121 +4,101 @@
4
4
  module Tapioca
5
5
  module RBIHelper
6
6
  extend T::Sig
7
- extend T::Helpers
7
+ include SorbetHelper
8
+ extend SorbetHelper
9
+ extend self
8
10
 
9
- requires_ancestor { Thor::Shell }
10
- requires_ancestor { SorbetHelper }
11
-
12
- sig do
13
- params(
14
- command: String,
15
- gem_dir: String,
16
- dsl_dir: String,
17
- auto_strictness: T::Boolean,
18
- gems: T::Array[Gemfile::GemSpec],
19
- compilers: T::Enumerable[Class]
20
- ).void
11
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
12
+ def create_param(name, type:)
13
+ create_typed_param(RBI::Param.new(name), type)
21
14
  end
22
- def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
23
- error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
24
-
25
- say("Checking generated RBI files... ")
26
- res = sorbet(
27
- "--no-config",
28
- "--error-url-base=#{error_url_base}",
29
- "--stop-after namer",
30
- dsl_dir,
31
- gem_dir
32
- )
33
- say(" Done", :green)
34
-
35
- errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
36
-
37
- if errors.empty?
38
- say(" No errors found\n\n", [:green, :bold])
39
- return
40
- end
41
-
42
- parse_errors = errors.select { |error| error.code < 4000 }
43
-
44
- if parse_errors.any?
45
- say_error(<<~ERR, :red)
46
-
47
- ##### INTERNAL ERROR #####
48
15
 
49
- There are parse errors in the generated RBI files.
50
-
51
- This seems related to a bug in Tapioca.
52
- Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
53
-
54
- Tapioca v#{Tapioca::VERSION}
55
-
56
- Command:
57
- #{command}
58
-
59
- ERR
60
-
61
- say_error(<<~ERR, :red) if gems.any?
62
- Gems:
63
- #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
16
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
17
+ def create_opt_param(name, type:, default:)
18
+ create_typed_param(RBI::OptParam.new(name, default), type)
19
+ end
64
20
 
65
- ERR
21
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
22
+ def create_rest_param(name, type:)
23
+ create_typed_param(RBI::RestParam.new(name), type)
24
+ end
66
25
 
67
- say_error(<<~ERR, :red) if compilers.any?
68
- Compilers:
69
- #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
26
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
27
+ def create_kw_param(name, type:)
28
+ create_typed_param(RBI::KwParam.new(name), type)
29
+ end
70
30
 
71
- ERR
31
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
32
+ def create_kw_opt_param(name, type:, default:)
33
+ create_typed_param(RBI::KwOptParam.new(name, default), type)
34
+ end
72
35
 
73
- say_error(<<~ERR, :red)
74
- Errors:
75
- #{parse_errors.map { |error| " #{error}" }.join("\n")}
36
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
37
+ def create_kw_rest_param(name, type:)
38
+ create_typed_param(RBI::KwRestParam.new(name), type)
39
+ end
76
40
 
77
- ##########################
41
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
42
+ def create_block_param(name, type:)
43
+ create_typed_param(RBI::BlockParam.new(name), type)
44
+ end
78
45
 
79
- ERR
80
- end
46
+ sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
47
+ def create_typed_param(param, type)
48
+ RBI::TypedParam.new(param: param, type: sanitize_signature_types(type))
49
+ end
81
50
 
82
- if auto_strictness
83
- redef_errors = errors.select { |error| error.code == 4010 }
84
- update_gem_rbis_strictnesses(redef_errors, gem_dir)
85
- end
51
+ sig { params(sig_string: String).returns(String) }
52
+ def sanitize_signature_types(sig_string)
53
+ sig_string
54
+ .gsub(".returns(<VOID>)", ".void")
55
+ .gsub("<VOID>", "void")
56
+ .gsub("<NOT-TYPED>", "T.untyped")
57
+ .gsub(".params()", "")
58
+ end
86
59
 
87
- Kernel.exit(1) if parse_errors.any?
60
+ sig do
61
+ params(
62
+ type: String,
63
+ variance: Symbol,
64
+ fixed: T.nilable(String),
65
+ upper: T.nilable(String),
66
+ lower: T.nilable(String)
67
+ ).returns(String)
88
68
  end
69
+ def self.serialize_type_variable(type, variance, fixed, upper, lower)
70
+ variance = nil if variance == :invariant
89
71
 
90
- private
72
+ bounds = []
73
+ bounds << "fixed: #{fixed}" if fixed
74
+ bounds << "lower: #{lower}" if lower
75
+ bounds << "upper: #{upper}" if upper
91
76
 
92
- sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
93
- def update_gem_rbis_strictnesses(errors, gem_dir)
94
- files = []
77
+ parameters = []
78
+ block = []
95
79
 
96
- errors.each do |error|
97
- # Collect the file with error
98
- files << error.file
99
- error.more.each do |line|
100
- # Also collect the conflicting definition file paths
101
- next unless line.include?("Previous definition")
80
+ parameters << ":#{variance}" if variance
102
81
 
103
- files << line.split(":").first&.strip
104
- end
82
+ if sorbet_supports?(:type_variable_block_syntax)
83
+ block = bounds
84
+ else
85
+ parameters.concat(bounds)
105
86
  end
106
87
 
107
- files
108
- .uniq
109
- .sort
110
- .select { |file| file.start_with?(gem_dir) }
111
- .each do |file|
112
- Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
113
- say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
114
- end
88
+ serialized = type.dup
89
+ serialized << "(#{parameters.join(", ")})" unless parameters.empty?
90
+ serialized << " { { #{block.join(", ")} } }" unless block.empty?
91
+ serialized
92
+ end
115
93
 
116
- say("\n")
94
+ sig { params(name: String).returns(T::Boolean) }
95
+ def valid_method_name?(name)
96
+ !name.to_sym.inspect.start_with?(':"', ":@", ":$")
117
97
  end
118
98
 
119
- sig { params(path: String).returns(String) }
120
- def gem_name_from_rbi_path(path)
121
- T.must(File.basename(path, ".rbi").split("@").first)
99
+ sig { params(name: String).returns(T::Boolean) }
100
+ def valid_parameter_name?(name)
101
+ /^([[:lower:]]|_|[^[[:ascii:]]])([[:alnum:]]|_|[^[[:ascii:]]])*$/.match?(name)
122
102
  end
123
103
  end
124
104
  end