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
data/lib/tapioca/cli.rb CHANGED
@@ -5,8 +5,7 @@ module Tapioca
5
5
  class Cli < Thor
6
6
  include CliHelper
7
7
  include ConfigHelper
8
- include SorbetHelper
9
- include ShimsHelper
8
+ include EnvHelper
10
9
 
11
10
  FILE_HEADER_OPTION_DESC = "Add a \"This file is generated\" header on top of each generated RBI file"
12
11
 
@@ -22,12 +21,23 @@ module Tapioca
22
21
  desc: "Verbose output for debugging purposes",
23
22
  default: false
24
23
 
25
- desc "init", "initializes folder structure"
24
+ desc "init", "get project ready for type checking"
26
25
  def init
27
- command = Commands::Init.new(
26
+ invoke(:configure)
27
+ invoke(:annotations)
28
+ invoke(:gem)
29
+ invoke(:todo)
30
+
31
+ print_init_next_steps
32
+ end
33
+
34
+ desc "configure", "initialize folder structure and type checking configuration"
35
+ option :postrequire, type: :string, default: DEFAULT_POSTREQUIRE_FILE
36
+ def configure
37
+ command = Commands::Configure.new(
28
38
  sorbet_config: SORBET_CONFIG_FILE,
29
- tapioca_config: TAPIOCA_CONFIG_FILE,
30
- default_postrequire: DEFAULT_POSTREQUIRE_FILE
39
+ tapioca_config: options[:config],
40
+ default_postrequire: options[:postrequire]
31
41
  )
32
42
  command.execute
33
43
  end
@@ -100,8 +110,15 @@ module Tapioca
100
110
  option :rbi_max_line_length,
101
111
  type: :numeric,
102
112
  desc: "Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped",
103
- default: 120
113
+ default: DEFAULT_RBI_MAX_LINE_LENGTH
114
+ option :environment,
115
+ aliases: ["-e"],
116
+ type: :string,
117
+ desc: "The Rack/Rails environment to use when generating RBIs",
118
+ default: DEFAULT_ENVIRONMENT
104
119
  def dsl(*constants)
120
+ set_environment(options)
121
+
105
122
  command = Commands::Dsl.new(
106
123
  requested_constants: constants,
107
124
  outpath: Pathname.new(options[:outdir]),
@@ -173,6 +190,10 @@ module Tapioca
173
190
  type: :boolean,
174
191
  desc: "Include YARD documentation from sources when generating RBIs. Warning: this might be slow",
175
192
  default: true
193
+ option :loc,
194
+ type: :boolean,
195
+ desc: "Include comments with source location when generating RBIs",
196
+ default: true
176
197
  option :exported_gem_rbis,
177
198
  type: :boolean,
178
199
  desc: "Include RBIs found in the `rbi/` directory of the gem",
@@ -194,9 +215,16 @@ module Tapioca
194
215
  option :rbi_max_line_length,
195
216
  type: :numeric,
196
217
  desc: "Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped",
197
- default: 120
218
+ default: DEFAULT_RBI_MAX_LINE_LENGTH
219
+ option :environment,
220
+ aliases: ["-e"],
221
+ type: :string,
222
+ desc: "The Rack/Rails environment to use when generating RBIs",
223
+ default: DEFAULT_ENVIRONMENT
198
224
  def gem(*gems)
199
225
  Tapioca.silence_warnings do
226
+ set_environment(options)
227
+
200
228
  all = options[:all]
201
229
  verify = options[:verify]
202
230
 
@@ -208,7 +236,8 @@ module Tapioca
208
236
  typed_overrides: options[:typed_overrides],
209
237
  outpath: Pathname.new(options[:outdir]),
210
238
  file_header: options[:file_header],
211
- doc: options[:doc],
239
+ include_doc: options[:doc],
240
+ include_loc: options[:loc],
212
241
  include_exported_rbis: options[:exported_gem_rbis],
213
242
  number_of_workers: options[:workers],
214
243
  auto_strictness: options[:auto_strictness],
@@ -231,7 +260,7 @@ module Tapioca
231
260
  end
232
261
 
233
262
  if gems.empty? && !all
234
- command.sync(should_verify: verify)
263
+ command.sync(should_verify: verify, exclude: options[:exclude])
235
264
  else
236
265
  command.execute
237
266
  end
@@ -243,68 +272,47 @@ module Tapioca
243
272
  option :gem_rbi_dir, type: :string, desc: "Path to gem RBIs", default: DEFAULT_GEM_DIR
244
273
  option :dsl_rbi_dir, type: :string, desc: "Path to DSL RBIs", default: DEFAULT_DSL_DIR
245
274
  option :shim_rbi_dir, type: :string, desc: "Path to shim RBIs", default: DEFAULT_SHIM_DIR
275
+ option :annotations_rbi_dir, type: :string, desc: "Path to annotations RBIs", default: DEFAULT_ANNOTATIONS_DIR
276
+ option :todo_rbi_file, type: :string, desc: "Path to the generated todo RBI file", default: DEFAULT_TODO_FILE
246
277
  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
247
279
  def check_shims
248
- index = RBI::Index.new
249
-
250
- shim_rbi_dir = options[:shim_rbi_dir]
251
- if !Dir.exist?(shim_rbi_dir) || Dir.empty?(shim_rbi_dir)
252
- say("No shim RBIs to check", :green)
253
- exit(0)
254
- end
255
-
256
- payload_path = T.let(nil, T.nilable(String))
257
-
258
- if options[:payload]
259
- if sorbet_supports?(:print_payload_sources)
260
- Dir.mktmpdir do |dir|
261
- payload_path = dir
262
- result = sorbet("--no-config --print=payload-sources:#{payload_path}")
263
-
264
- unless result.status
265
- say_error("Sorbet failed to dump payload")
266
- say_error(result.err)
267
- exit(1)
268
- end
269
-
270
- index_payload(index, payload_path)
271
- end
272
- else
273
- say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
274
- say_error("Current: v#{SORBET_GEM_SPEC.version}")
275
- say_error("Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}")
276
- exit(1)
277
- end
278
- end
280
+ command = Commands::CheckShims.new(
281
+ gem_rbi_dir: options[:gem_rbi_dir],
282
+ dsl_rbi_dir: options[:dsl_rbi_dir],
283
+ shim_rbi_dir: options[:shim_rbi_dir],
284
+ annotations_rbi_dir: options[:annotations_rbi_dir],
285
+ todo_rbi_file: options[:todo_rbi_file],
286
+ payload: options[:payload],
287
+ number_of_workers: options[:workers]
288
+ )
289
+ command.execute
290
+ end
279
291
 
280
- index_rbis(index, "shim", shim_rbi_dir)
281
- index_rbis(index, "gem", options[:gem_rbi_dir])
282
- index_rbis(index, "dsl", options[:dsl_rbi_dir])
283
-
284
- duplicates = duplicated_nodes_from_index(index, shim_rbi_dir)
285
- unless duplicates.empty?
286
- duplicates.each do |key, nodes|
287
- say_error("\nDuplicated RBI for #{key}:", :red)
288
- nodes.each do |node|
289
- node_loc = node.loc
290
- next unless node_loc
291
-
292
- loc_string = location_to_payload_url(node_loc, path_prefix: payload_path)
293
- say_error(" * #{loc_string}", :red)
294
- end
295
- end
296
- say_error("\nPlease remove the duplicated definitions from the #{shim_rbi_dir} directory.", :red)
292
+ desc "annotations", "Pull gem RBI annotations from remote sources"
293
+ option :sources, type: :array, default: [CENTRAL_REPO_ROOT_URI],
294
+ desc: "URIs of the sources to pull gem RBI annotations from"
295
+ option :netrc, type: :boolean, default: true, desc: "Use .netrc to authenticate to private sources"
296
+ option :netrc_file, type: :string, desc: "Path to .netrc file"
297
+ option :auth, type: :string, default: nil, desc: "HTTP authorization header for private sources"
298
+ option :typed_overrides,
299
+ aliases: ["--typed", "-t"],
300
+ type: :hash,
301
+ banner: "gem:level [gem:level ...]",
302
+ desc: "Override for typed sigils for pulled annotations",
303
+ default: {}
304
+ def annotations
305
+ if !options[:netrc] && options[:netrc_file]
306
+ say_error("Options `--no-netrc` and `--netrc-file` can't be used together", :bold, :red)
297
307
  exit(1)
298
308
  end
299
309
 
300
- say("\nNo duplicates found in shim RBIs", :green)
301
- exit(0)
302
- end
303
-
304
- desc "annotations", "Pull gem annotations from a central RBI repository"
305
- option :repo_uri, type: :string, desc: "Repository URI to pull annotations from", default: CENTRAL_REPO_ROOT_URI
306
- def annotations
307
- command = Commands::Annotations.new(central_repo_root_uri: options[:repo_uri])
310
+ command = Commands::Annotations.new(
311
+ central_repo_root_uris: options[:sources],
312
+ auth: options[:auth],
313
+ netrc_file: netrc_file(options),
314
+ typed_overrides: options[:typed_overrides]
315
+ )
308
316
  command.execute
309
317
  end
310
318
 
@@ -320,5 +328,61 @@ module Tapioca
320
328
  true
321
329
  end
322
330
  end
331
+
332
+ private
333
+
334
+ def print_init_next_steps
335
+ say(<<~OUTPUT)
336
+ #{set_color("This project is now set up for use with Sorbet and Tapioca", :bold)}
337
+
338
+ The sorbet/ folder should exist and look something like this:
339
+
340
+ ├── config # Default options to be passed to Sorbet on every run
341
+ └── rbi/
342
+ ├── annotations/ # Type definitions pulled from the rbi-central repository
343
+ ├── gems/ # Autogenerated type definitions for your gems
344
+ └── todo.rbi # Constants which were still missing after RBI generation
345
+ └── tapioca/
346
+ ├── config.yml # Default options to be passed to Tapioca
347
+ └── require.rb # A file where you can make requires from gems that might be needed for gem RBI generation
348
+
349
+ Please check this folder into version control.
350
+
351
+ #{set_color("🤔 What's next", :bold)}
352
+
353
+ 1. Many Ruby applications use metaprogramming DSLs to dynamically generate constants and methods.
354
+ To generate type definitions for any DSLs in your application, run:
355
+
356
+ #{set_color("bin/tapioca dsl", :cyan)}
357
+
358
+ 2. Check whether the constants in the #{set_color("sorbet/rbi/todo.rbi", :cyan)} file actually exist in your project.
359
+ It is possible that some of these constants are typos, and leaving them in #{set_color("todo.rbi", :cyan)} will
360
+ hide errors in your application. Ideally, you should be able to remove all definitions
361
+ from this file and delete it.
362
+
363
+ 3. Typecheck your project:
364
+
365
+ #{set_color("bundle exec srb tc", :cyan)}
366
+
367
+ There should not be any typechecking errors.
368
+
369
+ 4. Upgrade a file marked "#{set_color("# typed: false", :cyan)}" to "#{set_color("# typed: true", :cyan)}".
370
+ Then, run: #{set_color("bundle exec srb tc", :cyan)} and try to fix any errors.
371
+
372
+ You can use Spoom to bump files for you:
373
+
374
+ #{set_color("spoom bump --from false --to true", :cyan)}
375
+
376
+ To learn more about Spoom, visit: #{set_color("https://github.com/Shopify/spoom", :cyan)}
377
+
378
+ 5. Add signatures to your methods with #{set_color("sig", :cyan)}. To learn how, read: #{set_color("https://sorbet.org/docs/sigs", :cyan)}
379
+
380
+ #{set_color("Documentation", :bold)}
381
+ We recommend skimming these docs to get a feel for how to use Sorbet:
382
+ - Gradual Type Checking: #{set_color("https://sorbet.org/docs/gradual", :cyan)}
383
+ - Enabling Static Checks: #{set_color("https://sorbet.org/docs/static", :cyan)}
384
+ - RBI Files: #{set_color("https://sorbet.org/docs/rbi", :cyan)}
385
+ OUTPUT
386
+ end
323
387
  end
324
388
  end
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "net/http"
5
-
6
4
  module Tapioca
7
5
  module Commands
8
6
  class Annotations < Command
@@ -10,18 +8,33 @@ module Tapioca
10
8
 
11
9
  sig do
12
10
  params(
13
- central_repo_root_uri: String,
14
- central_repo_index_path: String
11
+ central_repo_root_uris: T::Array[String],
12
+ auth: T.nilable(String),
13
+ netrc_file: T.nilable(String),
14
+ central_repo_index_path: String,
15
+ typed_overrides: T::Hash[String, String]
15
16
  ).void
16
17
  end
17
- def initialize(central_repo_root_uri:, central_repo_index_path: CENTRAL_REPO_INDEX_PATH)
18
+ def initialize(
19
+ central_repo_root_uris:,
20
+ auth: nil,
21
+ netrc_file: nil,
22
+ central_repo_index_path: CENTRAL_REPO_INDEX_PATH,
23
+ typed_overrides: {}
24
+ )
18
25
  super()
19
- @central_repo_root_uri = central_repo_root_uri
20
- @index = T.let(fetch_index, RepoIndex)
26
+ @central_repo_root_uris = central_repo_root_uris
27
+ @auth = auth
28
+ @netrc_file = netrc_file
29
+ @netrc_info = T.let(nil, T.nilable(Netrc))
30
+ @tokens = T.let(repo_tokens, T::Hash[String, T.nilable(String)])
31
+ @indexes = T.let({}, T::Hash[String, RepoIndex])
32
+ @typed_overrides = typed_overrides
21
33
  end
22
34
 
23
35
  sig { override.void }
24
36
  def execute
37
+ @indexes = fetch_indexes
25
38
  project_gems = list_gemfile_gems
26
39
  remove_expired_annotations(project_gems)
27
40
  fetch_annotations(project_gems)
@@ -60,11 +73,33 @@ module Tapioca
60
73
  say("\nDone\n\n", :green)
61
74
  end
62
75
 
63
- sig { returns(RepoIndex) }
64
- def fetch_index
65
- say("Retrieving index from central repository... ", [:blue, :bold])
66
- content = fetch_file(CENTRAL_REPO_INDEX_PATH)
67
- exit(1) unless content
76
+ sig { returns(T::Hash[String, RepoIndex]) }
77
+ def fetch_indexes
78
+ multiple_repos = @central_repo_root_uris.size > 1
79
+ repo_number = 1
80
+ indexes = T.let({}, T::Hash[String, RepoIndex])
81
+
82
+ @central_repo_root_uris.each do |uri|
83
+ index = fetch_index(uri, repo_number: multiple_repos ? repo_number : nil)
84
+ next unless index
85
+
86
+ indexes[uri] = index
87
+ repo_number += 1
88
+ end
89
+
90
+ if indexes.empty?
91
+ say_error("\nCan't fetch annotations without sources (no index fetched)", :bold, :red)
92
+ exit(1)
93
+ end
94
+
95
+ indexes
96
+ end
97
+
98
+ sig { params(repo_uri: String, repo_number: T.nilable(Integer)).returns(T.nilable(RepoIndex)) }
99
+ def fetch_index(repo_uri, repo_number:)
100
+ say("Retrieving index from central repository#{repo_number ? " ##{repo_number}" : ""}... ", [:blue, :bold])
101
+ content = fetch_file(repo_uri, CENTRAL_REPO_INDEX_PATH)
102
+ return nil unless content
68
103
 
69
104
  index = RepoIndex.from_json(content)
70
105
  say("Done", :green)
@@ -74,7 +109,11 @@ module Tapioca
74
109
  sig { params(gem_names: T::Array[String]).returns(T::Array[String]) }
75
110
  def fetch_annotations(gem_names)
76
111
  say("Fetching gem annotations from central repository... ", [:blue, :bold])
77
- fetchable_gems = gem_names.select { |gem_name| @index.has_gem?(gem_name) }
112
+ fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
113
+
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) }
116
+ end
78
117
 
79
118
  if fetchable_gems.empty?
80
119
  say(" Nothing to do")
@@ -82,16 +121,21 @@ module Tapioca
82
121
  end
83
122
 
84
123
  say("\n")
85
- fetched_gems = fetchable_gems.select { |name| fetch_annotation(name) }
124
+ fetched_gems = fetchable_gems.select { |gem_name, repo_uris| fetch_annotation(repo_uris, gem_name) }
86
125
  say("\nDone", :green)
87
- fetched_gems
126
+ fetched_gems.keys.sort
88
127
  end
89
128
 
90
- sig { params(gem_name: String).void }
91
- def fetch_annotation(gem_name)
92
- content = fetch_file("#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
129
+ sig { params(repo_uris: T::Array[String], gem_name: String).void }
130
+ def fetch_annotation(repo_uris, gem_name)
131
+ contents = repo_uris.map do |repo_uri|
132
+ fetch_file(repo_uri, "#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
133
+ end
134
+
135
+ content = merge_files(gem_name, contents.compact)
93
136
  return unless content
94
137
 
138
+ content = apply_typed_override(gem_name, content)
95
139
  content = add_header(gem_name, content)
96
140
 
97
141
  dir = DEFAULT_ANNOTATIONS_DIR
@@ -100,36 +144,44 @@ module Tapioca
100
144
  create_file("#{dir}/#{gem_name}.rbi", content)
101
145
  end
102
146
 
103
- sig { params(path: String).returns(T.nilable(String)) }
104
- def fetch_file(path)
105
- if @central_repo_root_uri.start_with?(%r{https?://})
106
- fetch_http_file(path)
147
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
148
+ def fetch_file(repo_uri, path)
149
+ if repo_uri.start_with?(%r{https?://})
150
+ fetch_http_file(repo_uri, path)
107
151
  else
108
- fetch_local_file(path)
152
+ fetch_local_file(repo_uri, path)
109
153
  end
110
154
  end
111
155
 
112
- sig { params(path: String).returns(T.nilable(String)) }
113
- def fetch_local_file(path)
114
- File.read("#{@central_repo_root_uri}/#{path}")
156
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
157
+ def fetch_local_file(repo_uri, path)
158
+ File.read("#{repo_uri}/#{path}")
115
159
  rescue => e
116
160
  say_error("\nCan't fetch file `#{path}` (#{e.message})", :bold, :red)
117
161
  nil
118
162
  end
119
163
 
120
- sig { params(path: String).returns(T.nilable(String)) }
121
- def fetch_http_file(path)
122
- uri = URI("#{@central_repo_root_uri}/#{path}")
123
- response = Net::HTTP.get_response(uri)
164
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
165
+ def fetch_http_file(repo_uri, path)
166
+ auth = @tokens[repo_uri]
167
+ uri = URI("#{repo_uri}/#{path}")
168
+
169
+ request = Net::HTTP::Get.new(uri)
170
+ request["Authorization"] = auth if auth
171
+
172
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
173
+ http.request(request)
174
+ end
175
+
124
176
  case response
125
177
  when Net::HTTPSuccess
126
178
  response.body
127
179
  else
128
- say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{response.class})", :bold, :red)
180
+ say_http_error(path, repo_uri, message: response.class)
129
181
  nil
130
182
  end
131
183
  rescue SocketError, Errno::ECONNREFUSED => e
132
- say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{e.message})", :bold, :red)
184
+ say_http_error(path, repo_uri, message: e.message)
133
185
  nil
134
186
  end
135
187
 
@@ -137,11 +189,12 @@ module Tapioca
137
189
  def add_header(name, content)
138
190
  header = <<~COMMENT
139
191
  # DO NOT EDIT MANUALLY
140
- # This file was pulled from #{@central_repo_root_uri}.
192
+ # This file was pulled from a central RBI files repository.
141
193
  # Please run `#{default_command(:annotations)}` to update it.
142
194
  COMMENT
143
195
 
144
- contents = content.split("\n")
196
+ # Split contents into newlines and ensure trailing empty lines are included
197
+ contents = content.split("\n", -1)
145
198
  if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
146
199
  contents.insert(2, header).join("\n")
147
200
  else
@@ -149,6 +202,86 @@ module Tapioca
149
202
  content
150
203
  end
151
204
  end
205
+
206
+ sig { params(name: String, content: String).returns(String) }
207
+ def apply_typed_override(name, content)
208
+ strictness = @typed_overrides[name]
209
+ return content unless strictness
210
+
211
+ unless Spoom::Sorbet::Sigils.strictness_in_content(content)
212
+ return "# typed: #{strictness}\n\n#{content}"
213
+ end
214
+
215
+ Spoom::Sorbet::Sigils.update_sigil(content, strictness)
216
+ end
217
+
218
+ sig { params(gem_name: String, contents: T::Array[String]).returns(T.nilable(String)) }
219
+ def merge_files(gem_name, contents)
220
+ return nil if contents.empty?
221
+
222
+ rewriter = RBI::Rewriters::Merge.new(keep: RBI::Rewriters::Merge::Keep::NONE)
223
+
224
+ contents.each do |content|
225
+ rbi = RBI::Parser.parse_string(content)
226
+ rewriter.merge(rbi)
227
+ end
228
+
229
+ tree = rewriter.tree
230
+ return tree.string if tree.conflicts.empty?
231
+
232
+ say_error("\n\n Can't import RBI file for `#{gem_name}` as it contains conflicts:", :yellow)
233
+
234
+ tree.conflicts.each do |conflict|
235
+ say_error(" #{conflict}", :yellow)
236
+ end
237
+
238
+ nil
239
+ rescue RBI::ParseError => e
240
+ say_error("\n\n Can't import RBI file for `#{gem_name}` as it contains errors:", :yellow)
241
+ say_error(" Error: #{e.message} (#{e.location})")
242
+ nil
243
+ end
244
+
245
+ sig { returns(T::Hash[String, T.nilable(String)]) }
246
+ def repo_tokens
247
+ @netrc_info = Netrc.read(@netrc_file) if @netrc_file
248
+ @central_repo_root_uris.map do |uri|
249
+ if @auth
250
+ [uri, @auth]
251
+ else
252
+ [uri, token_for(uri)]
253
+ end
254
+ end.compact.to_h
255
+ end
256
+
257
+ sig { params(repo_uri: String).returns(T.nilable(String)) }
258
+ def token_for(repo_uri)
259
+ return nil unless @netrc_info
260
+
261
+ host = URI(repo_uri).host
262
+ return nil unless host
263
+
264
+ creds = @netrc_info[host]
265
+ return nil unless creds
266
+
267
+ token = creds.to_a.last
268
+ return nil unless token
269
+
270
+ "token #{token}"
271
+ end
272
+
273
+ sig { params(path: String, repo_uri: String, message: String).void }
274
+ def say_http_error(path, repo_uri, message:)
275
+ say_error("\nCan't fetch file `#{path}` from #{repo_uri} (#{message})\n\n", :bold, :red)
276
+ say_error(<<~ERROR)
277
+ Tapioca can't access the annotations at #{repo_uri}.
278
+
279
+ Are you trying to access a private repository?
280
+ If so, please specify your Github credentials in your ~/.netrc file or by specifying the --auth option.
281
+
282
+ See https://github.com/Shopify/tapioca#using-a-netrc-file for more details.
283
+ ERROR
284
+ end
152
285
  end
153
286
  end
154
287
  end
@@ -0,0 +1,101 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Commands
6
+ class CheckShims < Command
7
+ extend T::Sig
8
+ include SorbetHelper
9
+ include RBIFilesHelper
10
+
11
+ sig do
12
+ params(
13
+ gem_rbi_dir: String,
14
+ dsl_rbi_dir: String,
15
+ annotations_rbi_dir: String,
16
+ shim_rbi_dir: String,
17
+ todo_rbi_file: String,
18
+ payload: T::Boolean,
19
+ number_of_workers: T.nilable(Integer)
20
+ ).void
21
+ 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
+ )
31
+ super()
32
+ @gem_rbi_dir = gem_rbi_dir
33
+ @dsl_rbi_dir = dsl_rbi_dir
34
+ @annotations_rbi_dir = annotations_rbi_dir
35
+ @shim_rbi_dir = shim_rbi_dir
36
+ @todo_rbi_file = todo_rbi_file
37
+ @payload = payload
38
+ @number_of_workers = number_of_workers
39
+ end
40
+
41
+ sig { override.void }
42
+ def execute
43
+ index = RBI::Index.new
44
+
45
+ if (!Dir.exist?(@shim_rbi_dir) || Dir.empty?(@shim_rbi_dir)) && !File.exist?(@todo_rbi_file)
46
+ say("No shim RBIs to check", :green)
47
+ exit(0)
48
+ end
49
+
50
+ payload_path = T.let(nil, T.nilable(String))
51
+
52
+ if @payload
53
+ if sorbet_supports?(:print_payload_sources)
54
+ Dir.mktmpdir do |dir|
55
+ payload_path = dir
56
+ result = sorbet("--no-config --print=payload-sources:#{payload_path}")
57
+
58
+ unless result.status
59
+ say_error("Sorbet failed to dump payload")
60
+ say_error(result.err)
61
+ exit(1)
62
+ end
63
+
64
+ index_rbis(index, "payload", payload_path, number_of_workers: @number_of_workers)
65
+ end
66
+ else
67
+ say_error("The version of Sorbet used in your Gemfile.lock does not support `--print=payload-sources`")
68
+ say_error("Current: v#{SORBET_GEM_SPEC.version}")
69
+ say_error("Required: #{FEATURE_REQUIREMENTS[:print_payload_sources]}")
70
+ exit(1)
71
+ end
72
+ end
73
+
74
+ 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)
79
+
80
+ duplicates = duplicated_nodes_from_index(index, shim_rbi_dir: @shim_rbi_dir, todo_rbi_file: @todo_rbi_file)
81
+ unless duplicates.empty?
82
+ duplicates.each do |key, nodes|
83
+ say_error("\nDuplicated RBI for #{key}:", :red)
84
+ nodes.each do |node|
85
+ node_loc = node.loc
86
+ next unless node_loc
87
+
88
+ loc_string = location_to_payload_url(node_loc, path_prefix: payload_path)
89
+ say_error(" * #{loc_string}", :red)
90
+ end
91
+ end
92
+ say_error("\nPlease remove the duplicated definitions from #{@shim_rbi_dir} and #{@todo_rbi_file}", :red)
93
+ exit(1)
94
+ end
95
+
96
+ say("\nNo duplicates found in shim RBIs", :green)
97
+ exit(0)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -3,7 +3,7 @@
3
3
 
4
4
  module Tapioca
5
5
  module Commands
6
- class Init < Command
6
+ class Configure < Command
7
7
  sig do
8
8
  params(
9
9
  sorbet_config: String,
@@ -5,7 +5,7 @@ module Tapioca
5
5
  module Commands
6
6
  class Dsl < Command
7
7
  include SorbetHelper
8
- include RBIHelper
8
+ include RBIFilesHelper
9
9
 
10
10
  sig do
11
11
  params(