tapioca 0.19.2 → 0.20.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -45
  3. data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
  4. data/lib/tapioca/cli.rb +10 -11
  5. data/lib/tapioca/commands/abstract_dsl.rb +71 -20
  6. data/lib/tapioca/commands/abstract_gem.rb +1 -0
  7. data/lib/tapioca/commands/annotations.rb +1 -1
  8. data/lib/tapioca/commands/dsl_generate.rb +0 -15
  9. data/lib/tapioca/commands/gem_generate.rb +20 -4
  10. data/lib/tapioca/dsl/compiler.rb +24 -10
  11. data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
  12. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  14. data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
  15. data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
  16. data/lib/tapioca/dsl/compilers/config.rb +1 -1
  17. data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
  18. data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
  19. data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
  20. data/lib/tapioca/dsl/compilers.rb +1 -1
  21. data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +7 -2
  22. data/lib/tapioca/dsl/pipeline.rb +3 -0
  23. data/lib/tapioca/gem/events.rb +1 -1
  24. data/lib/tapioca/gem/listeners/documentation.rb +13 -8
  25. data/lib/tapioca/gem/listeners/methods.rb +79 -13
  26. data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
  27. data/lib/tapioca/gem/pipeline.rb +3 -3
  28. data/lib/tapioca/gemfile.rb +13 -0
  29. data/lib/tapioca/helpers/env_helper.rb +1 -1
  30. data/lib/tapioca/helpers/file_helper.rb +42 -0
  31. data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
  32. data/lib/tapioca/helpers/rbi_helper.rb +17 -3
  33. data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
  34. data/lib/tapioca/internal.rb +1 -0
  35. data/lib/tapioca/loaders/gem.rb +33 -0
  36. data/lib/tapioca/loaders/loader.rb +0 -13
  37. data/lib/tapioca/rbi_ext/model.rb +16 -2
  38. data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
  39. data/lib/tapioca/rbs/rewriter.rb +32 -21
  40. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
  41. data/lib/tapioca/runtime/generic_type_registry.rb +27 -2
  42. data/lib/tapioca/runtime/reflection.rb +18 -1
  43. data/lib/tapioca/version.rb +1 -1
  44. data/lib/tapioca.rb +11 -10
  45. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b165a37e65e25d71de2a502703464d1f877232f4b1d1a173052821e0c7e2170
4
- data.tar.gz: f75734a7d89e0dba3f4fbee6c86ab3754dfad69cdaa0bd2575c872ec3020bb65
3
+ metadata.gz: 16f43e72a4dc002c9e6dcfa922274e3aff4de57318d48774eb7592ca4d548d8e
4
+ data.tar.gz: '03464479e0edf22b0abfe21536b674fd9000d2e7bd980585b4df01ab17f176e9'
5
5
  SHA512:
6
- metadata.gz: 54ab42e4c24755c84f831c8fc5f40db823061558507f4b810af16f7b98c3adac9e02a53b64f9be1f0fb824efec2ca87bec4d6b133b63c37ddb247af1e38b3c45
7
- data.tar.gz: 8465d7d2a74d715a966addd29069ec850f4d002662674dab172d0165f5d22228ba9855d70c16a15e8564613b758c76921f9919995da079ad04d8d03e039c47ba
6
+ metadata.gz: 9ffbfb1deb3d1e54ed3d3d4a0301a6adcd81dbdba981f187223065d2e9bf67c406db3bde657103dd66b12e8a96ecb4953e0491a3c79feb104b95cbc5608270af
7
+ data.tar.gz: 548838d69fd2fe5f1be747aef82748362ce4394a3844fefca09037ab8bc33972cc70bcd869c4e22fb09112e2483e8703b3738e96c4e58f5e75004f7729e83087
data/README.md CHANGED
@@ -41,6 +41,7 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
41
41
  * [Changing the strictness level of the RBI for a gem](#changing-the-strictness-level-of-the-rbi-for-a-gem)
42
42
  * [Keeping RBI files for gems up-to-date](#keeping-rbi-files-for-gems-up-to-date)
43
43
  * [Importing hand written signatures from gem's `rbi/` folder](#importing-hand-written-signatures-from-gems-rbi-folder)
44
+ * [Writing custom gem extensions](#writing-custom-gem-extensions)
44
45
  * [Pulling RBI annotations from remote sources](#pulling-rbi-annotations-from-remote-sources)
45
46
  * [Basic authentication](#basic-authentication)
46
47
  * [Using a .netrc file](#using-a-netrc-file)
@@ -52,7 +53,6 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
52
53
  * [Writing custom DSL extensions](#writing-custom-dsl-extensions)
53
54
  * [Rewriting RBS comments to Sorbet signatures](#rewriting-rbs-comments-to-sorbet-signatures)
54
55
  * [Caching rewrites with Bootsnap](#caching-rewrites-with-bootsnap)
55
- * [Priming the cache from CI](#priming-the-cache-from-ci)
56
56
  * [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
57
57
  * [Configuration](#configuration)
58
58
  * [Editor Integration](#editor-integration)
@@ -346,6 +346,55 @@ This option can be used in CI to make sure the RBI files are *up-to-date* and en
346
346
 
347
347
  Tapioca will import any signatures found in the `rbi/` folder of a given gem and combine them with the RBIs it generates. This is useful when a gem doesn't want to depend on `sorbet-runtime` but still wants to provide type safety to users during static checks. Note that the `rbi/` folder needs to be included in the gem release using the `.gemspec` file. Applications can choose not to import these signatures using the `--no-exported-gem-rbis` flag.
348
348
 
349
+ #### Writing custom gem extensions
350
+
351
+ Similar to [DSL extensions](#writing-custom-dsl-extensions), it is sometimes useful to patch and modify certain behavior
352
+ from gems to ensure that RBIs are generated correctly. Gem RBIs are generated by reflecting over what the gem defines at
353
+ runtime, which means that anything the gem never really defines is invisible to Tapioca. A common example is methods
354
+ handled by `method_missing`:
355
+
356
+ ```ruby
357
+ # my_gem/lib/my_gem/settings.rb
358
+
359
+ module MyGem
360
+ class Settings
361
+ SETTINGS = [:host, :port, :timeout]
362
+
363
+ def method_missing(name, *args)
364
+ SETTINGS.include?(name) ? @config[name] : super
365
+ end
366
+
367
+ def respond_to_missing?(name, include_private = false)
368
+ SETTINGS.include?(name) || super
369
+ end
370
+ end
371
+ end
372
+ ```
373
+
374
+ Since `host`, `port` and `timeout` are never defined as real methods, they will be missing from the generated RBI. An
375
+ extension can define them, so that Tapioca is able to see them:
376
+
377
+ ```ruby
378
+ # my_gem/lib/tapioca/gem/extensions/my_gem.rb
379
+
380
+ require "my_gem"
381
+
382
+ module MyGem
383
+ class Settings
384
+ SETTINGS.each do |name|
385
+ define_method(name) { @config[name] }
386
+ end
387
+ end
388
+ end
389
+ ```
390
+
391
+ Extensions are only loaded during RBI generation, so they never change the behavior of the gem for applications using
392
+ it. They are also loaded before the bundle is required, which is why the extension has to require what it wants to
393
+ patch.
394
+
395
+ Applications can also define their own gem extensions by placing them inside the `sorbet/tapioca/gem/extensions`
396
+ directory.
397
+
349
398
  ### Pulling RBI annotations from remote sources
350
399
 
351
400
  Since Tapioca does not perform any type inference, the RBI files generated for the gems do not contain any type signatures. Instead, Tapioca relies on the community to provide high-quality, manually written RBI annotations for public gems.
@@ -492,35 +541,35 @@ Usage:
492
541
  tapioca dsl [constant...]
493
542
 
494
543
  Options:
495
- --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files
496
- # Default: sorbet/rbi/dsl
497
- [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file
498
- # Default: true
499
- [--only=compiler [compiler ...]] # Only run supplied DSL compiler(s)
500
- [--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s)
501
- [--verify], [--no-verify], [--skip-verify] # Verifies RBIs are up-to-date
502
- # Default: false
503
- [--only-bootsnap-rbs-cache], [--no-only-bootsnap-rbs-cache], [--skip-only-bootsnap-rbs-cache] # Only boot the application and load DSL extensions/compilers to populate the bootsnap iseq cache, then exit. Skips compiler execution and RBI generation. Mutually exclusive with --verify and --list-compilers.
504
- # Default: false
505
- -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output
506
- # Default: false
507
- -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
508
- [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
509
- # Default: 120
510
- -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
511
- # Default: development
512
- -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers
513
- # Default: false
514
- [--app-root=APP_ROOT] # The path to the Rails application
515
- # Default: .
516
- [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
517
- # Default: true
518
- [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
519
- [--compiler-options=key:value] # Options to pass to the DSL compilers
520
- -c, [--config=<config file path>] # Path to the Tapioca configuration file
521
- # Default: sorbet/tapioca/config.yml
522
- -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
523
- # Default: false
544
+ --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files
545
+ # Default: sorbet/rbi/dsl
546
+ [--file-header], [--no-file-header], [--skip-file-header] # Add a "This file is generated" header on top of each generated RBI file
547
+ # Default: true
548
+ [--only=compiler [compiler ...]] # Only run supplied DSL compiler(s)
549
+ [--exclude=compiler [compiler ...]] # Exclude supplied DSL compiler(s)
550
+ [--verify], [--no-verify], [--skip-verify] # Verifies RBIs are up-to-date
551
+ # Default: false
552
+ -q, [--quiet], [--no-quiet], [--skip-quiet] # Suppresses file creation output
553
+ # Default: false
554
+ -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
555
+ [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
556
+ # Default: 120
557
+ [--max-diff-lines=N] # Max number of diff lines to include in the `dsl --verify` output
558
+ # Default: 250
559
+ -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
560
+ # Default: development
561
+ -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers
562
+ # Default: false
563
+ [--app-root=APP_ROOT] # The path to the Rails application
564
+ # Default: .
565
+ [--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
566
+ # Default: true
567
+ [--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
568
+ [--compiler-options=key:value] # Options to pass to the DSL compilers
569
+ -c, [--config=<config file path>] # Path to the Tapioca configuration file
570
+ # Default: sorbet/tapioca/config.yml
571
+ -V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
572
+ # Default: false
524
573
 
525
574
  Generate RBIs for dynamic methods
526
575
  ```
@@ -855,7 +904,13 @@ The rewriting is automatic on every `tapioca` invocation: [`require-hooks`](http
855
904
  $ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl
856
905
  ```
857
906
 
858
- Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cache/bootsnap-tapioca-rbs` by default; override with `TAPIOCA_BOOTSNAP_CACHE_DIR`). The first run is slower because every file is rewritten and the result is baked into the iseq cache; subsequent runs against the same directory skip the rewrite entirely.
907
+ Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cache/bootsnap-tapioca-rbs` by
908
+ default; override with `TAPIOCA_BOOTSNAP_CACHE_DIR`).
909
+
910
+ Tapioca writes the current `Gemfile.lock` digest to `.gemfile-lock-digest` inside that cache directory. When the
911
+ lockfile changes, Tapioca sees the digest mismatch and resets Bootsnap's cache payload before configuring Bootsnap.
912
+ This lets gem bumps that affect rewriting, such as `tapioca`, start from a fresh cache without accumulating old cache
913
+ directories.
859
914
 
860
915
  `Bootsnap.setup` mutates a process-wide singleton, and a second call would overwrite Tapioca's dedicated cache directory and start writing rewritten iseqs into the host's normal cache. Tapioca enforces this under `TAPIOCA_RBS_CACHE=1`: after its own setup runs, any subsequent `Bootsnap.setup` raises a clear error pointing at the fix. Gate your host's `Bootsnap.setup` on the same env var. Rails apps do this in `config/boot.rb`:
861
916
 
@@ -864,19 +919,6 @@ Tapioca configures Bootsnap's iseq cache against a dedicated directory (`tmp/cac
864
919
  require "bootsnap/setup" unless ENV["TAPIOCA_RBS_CACHE"] == "1"
865
920
  ```
866
921
 
867
- #### Priming the cache from CI
868
-
869
- For CI pipelines that want to populate the cache once and have downstream jobs read from a warm copy, use `--only-bootsnap-rbs-cache`. This pattern lets you scope cache writes to a single job (the prime) so PR-side jobs read from it without uploading on every successful build:
870
-
871
- ```shell
872
- # Prime: populate the cache.
873
- $ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl --only-bootsnap-rbs-cache
874
-
875
- # Consumer: read from the populated cache.
876
- # BOOTSNAP_READONLY=1 prevents bootsnap from writing back to a read-only mount.
877
- $ TAPIOCA_RBS_CACHE=1 BOOTSNAP_READONLY=1 bin/tapioca dsl
878
- ```
879
-
880
922
  ### RBI files for missing constants and methods
881
923
 
882
924
  Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet.
@@ -998,10 +1040,10 @@ dsl:
998
1040
  only: []
999
1041
  exclude: []
1000
1042
  verify: false
1001
- only_bootsnap_rbs_cache: false
1002
1043
  quiet: false
1003
1044
  workers: 1
1004
1045
  rbi_max_line_length: 120
1046
+ max_diff_lines: 250
1005
1047
  environment: development
1006
1048
  list_compilers: false
1007
1049
  app_root: "."
@@ -116,7 +116,7 @@ module RubyLsp
116
116
 
117
117
  #: (*untyped flags) -> Array[String]
118
118
  def git_ls_gem_rbis(*flags)
119
- self #: as untyped # rubocop:disable Style/RedundantSelf
119
+ self #: as untyped
120
120
  .execute_in_project_path("git", "ls-files", *flags, "sorbet/rbi/gems/")
121
121
  .lines
122
122
  .map(&:strip)
data/lib/tapioca/cli.rb CHANGED
@@ -103,10 +103,6 @@ module Tapioca
103
103
  type: :boolean,
104
104
  default: false,
105
105
  desc: "Verifies RBIs are up-to-date"
106
- option :only_bootsnap_rbs_cache,
107
- type: :boolean,
108
- default: false,
109
- desc: "Only boot the application and load DSL extensions/compilers to populate the bootsnap iseq cache, then exit. Skips compiler execution and RBI generation. Mutually exclusive with --verify and --list-compilers."
110
106
  option :quiet,
111
107
  aliases: ["-q"],
112
108
  type: :boolean,
@@ -120,6 +116,10 @@ module Tapioca
120
116
  type: :numeric,
121
117
  desc: "Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped",
122
118
  default: DEFAULT_RBI_MAX_LINE_LENGTH
119
+ option :max_diff_lines,
120
+ type: :numeric,
121
+ desc: "Max number of diff lines to include in the `dsl --verify` output",
122
+ default: DEFAULT_MAX_DIFF_LINES
123
123
  option :environment,
124
124
  aliases: ["-e"],
125
125
  type: :string,
@@ -150,15 +150,13 @@ module Tapioca
150
150
  def dsl(*constant_or_paths)
151
151
  set_environment(options)
152
152
 
153
- if options[:only_bootsnap_rbs_cache] && (options[:verify] || options[:list_compilers])
154
- conflicting = options[:verify] ? "--verify" : "--list-compilers"
155
- raise MalformattedArgumentError,
156
- "Options '--only-bootsnap-rbs-cache' and '#{conflicting}' are mutually exclusive"
157
- end
158
-
159
153
  # Assume anything starting with a capital letter or colon is a class, otherwise a path
160
154
  constants, paths = constant_or_paths.partition { |c| c =~ /\A[A-Z:]/ }
161
155
 
156
+ unless options[:max_diff_lines].positive?
157
+ raise MalformattedArgumentError, "Option '--max-diff-lines' must be a positive number"
158
+ end
159
+
162
160
  command_args = {
163
161
  requested_constants: constants,
164
162
  requested_paths: paths.map { |p| Pathname.new(p) },
@@ -176,6 +174,7 @@ module Tapioca
176
174
  halt_upon_load_error: options[:halt_upon_load_error],
177
175
  compiler_options: options[:compiler_options],
178
176
  lsp_addon: self.class.addon_mode,
177
+ max_diff_lines: options[:max_diff_lines],
179
178
  }
180
179
 
181
180
  command = if options[:verify]
@@ -183,7 +182,7 @@ module Tapioca
183
182
  elsif options[:list_compilers]
184
183
  Commands::DslCompilerList.new(**command_args)
185
184
  else
186
- Commands::DslGenerate.new(**command_args, only_bootsnap_rbs_cache: options[:only_bootsnap_rbs_cache])
185
+ Commands::DslGenerate.new(**command_args)
187
186
  end
188
187
 
189
188
  command.run
@@ -5,6 +5,7 @@ module Tapioca
5
5
  module Commands
6
6
  # @abstract
7
7
  class AbstractDsl < CommandWithoutTracker
8
+ include FileHelper
8
9
  include SorbetHelper
9
10
  include RBIFilesHelper
10
11
 
@@ -26,7 +27,8 @@ module Tapioca
26
27
  #| ?app_root: String,
27
28
  #| ?halt_upon_load_error: bool,
28
29
  #| ?compiler_options: Hash[String, untyped],
29
- #| ?lsp_addon: bool
30
+ #| ?lsp_addon: bool,
31
+ #| ?max_diff_lines: Integer
30
32
  #| ) -> void
31
33
  def initialize(
32
34
  requested_constants:,
@@ -46,7 +48,8 @@ module Tapioca
46
48
  app_root: ".",
47
49
  halt_upon_load_error: true,
48
50
  compiler_options: {},
49
- lsp_addon: false
51
+ lsp_addon: false,
52
+ max_diff_lines: DEFAULT_MAX_DIFF_LINES
50
53
  )
51
54
  @requested_constants = requested_constants
52
55
  @requested_paths = requested_paths
@@ -66,6 +69,7 @@ module Tapioca
66
69
  @skip_constant = skip_constant
67
70
  @compiler_options = compiler_options
68
71
  @lsp_addon = lsp_addon
72
+ @max_diff_lines = max_diff_lines
69
73
 
70
74
  super()
71
75
  end
@@ -247,7 +251,7 @@ module Tapioca
247
251
  def perform_dsl_verification(dir)
248
252
  diff = verify_dsl_rbi(tmp_dir: dir)
249
253
 
250
- report_diff_and_exit_if_out_of_date(diff, :dsl)
254
+ report_diff_and_exit_if_out_of_date(diff, tmp_dir: dir, command: :dsl)
251
255
  ensure
252
256
  FileUtils.remove_entry(dir)
253
257
  end
@@ -269,7 +273,7 @@ module Tapioca
269
273
  @outpath / "#{underscore(constant_name)}.rbi"
270
274
  end
271
275
 
272
- #: (tmp_dir: Pathname) -> Hash[String, Symbol]
276
+ #: (tmp_dir: Pathname) -> Hash[Pathname, Symbol]
273
277
  def verify_dsl_rbi(tmp_dir:)
274
278
  diff = {}
275
279
 
@@ -301,7 +305,7 @@ module Tapioca
301
305
  diff
302
306
  end
303
307
 
304
- #: (Symbol cause, Array[String] files) -> String
308
+ #: (Symbol cause, Array[Pathname] files) -> String
305
309
  def build_error_for_files(cause, files)
306
310
  filenames = files.map do |file|
307
311
  @outpath / file
@@ -310,26 +314,73 @@ module Tapioca
310
314
  " File(s) #{cause}:\n - #{filenames}"
311
315
  end
312
316
 
313
- #: (Hash[String, Symbol] diff, Symbol command) -> void
314
- def report_diff_and_exit_if_out_of_date(diff, command)
317
+ #: (Hash[Pathname, Symbol] diff, tmp_dir: Pathname, command: Symbol) -> void
318
+ def report_diff_and_exit_if_out_of_date(diff, tmp_dir:, command:)
315
319
  if diff.empty?
316
320
  say("Nothing to do, all RBIs are up-to-date.")
317
- else
318
- reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
319
- build_error_for_files(cause, diff_for_cause.map(&:first))
320
- end.join("\n")
321
+ return
322
+ end
323
+
324
+ reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
325
+ build_error_for_files(cause, diff_for_cause.map(&:first))
326
+ end.join("\n")
327
+
328
+ diff_output, diff_truncated = build_diff_output(diff, tmp_dir)
329
+
330
+ diff_section =
331
+ if diff_truncated
332
+ "#{set_color("Diff truncated to #{@max_diff_lines} lines.\nChange the number of displayed lines using the `--max-diff-lines` flag.", :red)}\n#{diff_output.rstrip}"
333
+ elsif !diff_output.empty?
334
+ "#{set_color("Diff:", :red)}\n#{diff_output.rstrip}"
335
+ else
336
+ ""
337
+ end
338
+
339
+ raise Tapioca::Error, <<~ERROR.rstrip
340
+ #{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
341
+ #{set_color("`#{default_command(command)}`", :green, :bold)}
342
+ #{set_color("Once it is complete, be sure to commit and push any changes", :green)}
343
+ If you don't observe any changes after running the command locally, ensure your database is in a good
344
+ state e.g. run `bin/rails db:reset`
321
345
 
322
- raise Tapioca::Error, <<~ERROR
323
- #{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
324
- #{set_color("`#{default_command(command)}`", :green, :bold)}
325
- #{set_color("Once it is complete, be sure to commit and push any changes", :green)}
326
- If you don't observe any changes after running the command locally, ensure your database is in a good
327
- state e.g. run `bin/rails db:reset`
346
+ #{set_color("Reason:", :red)}
347
+ #{reasons}
328
348
 
329
- #{set_color("Reason:", :red)}
330
- #{reasons}
331
- ERROR
349
+ #{diff_section}
350
+ ERROR
351
+ end
352
+
353
+ #: (Hash[Pathname, Symbol] diff, Pathname tmp_dir) -> [String, bool]
354
+ def build_diff_output(diff, tmp_dir)
355
+ out = String.new
356
+ truncated = false #: bool
357
+ line_count = 0
358
+
359
+ diff.each do |file, status|
360
+ old_path = (@outpath / file)
361
+ new_path = (tmp_dir / file)
362
+
363
+ chunk = case status
364
+ when :added then file_diff(file, File::NULL, new_path)
365
+ when :removed then file_diff(file, old_path, File::NULL)
366
+ when :changed then file_diff(file, old_path, new_path)
367
+ else ""
368
+ end
369
+
370
+ next if chunk.nil?
371
+
372
+ remaining_lines = @max_diff_lines - line_count
373
+ if chunk.lines.count > remaining_lines
374
+ out << chunk.lines.first(remaining_lines).join
375
+ truncated = true
376
+ break
377
+ end
378
+
379
+ out << chunk
380
+ line_count += chunk.lines.count
332
381
  end
382
+
383
+ [out, truncated]
333
384
  end
334
385
 
335
386
  #: (Pathname path) -> Array[Pathname]
@@ -71,6 +71,7 @@ module Tapioca
71
71
  @include_doc = include_doc #: bool
72
72
  @include_loc = include_loc #: bool
73
73
  @include_exported_rbis = include_exported_rbis
74
+ @skipped_gems = [] #: Array[String]
74
75
  @halt_upon_load_error = halt_upon_load_error
75
76
  end
76
77
 
@@ -187,7 +187,7 @@ module Tapioca
187
187
  say_http_error(path, repo_uri, message: response.class)
188
188
  nil
189
189
  end
190
- rescue SocketError, Errno::ECONNREFUSED => e
190
+ rescue => e
191
191
  say_http_error(path, repo_uri, message: e.message)
192
192
  nil
193
193
  end
@@ -4,12 +4,6 @@
4
4
  module Tapioca
5
5
  module Commands
6
6
  class DslGenerate < AbstractDsl
7
- #: (?only_bootsnap_rbs_cache: bool, **untyped) -> void
8
- def initialize(only_bootsnap_rbs_cache: false, **kwargs)
9
- @only_bootsnap_rbs_cache = only_bootsnap_rbs_cache
10
- super(**T.unsafe(kwargs))
11
- end
12
-
13
7
  private
14
8
 
15
9
  # @override
@@ -17,15 +11,6 @@ module Tapioca
17
11
  def execute
18
12
  load_application
19
13
 
20
- if @only_bootsnap_rbs_cache
21
- if ENV["TAPIOCA_RBS_CACHE"] == "1"
22
- say("Bootsnap RBS cache populated, exiting before RBI generation.", :green)
23
- else
24
- say_error("Warning: --only-bootsnap-rbs-cache requires TAPIOCA_RBS_CACHE=1 to populate the cache", :yellow)
25
- end
26
- return
27
- end
28
-
29
14
  say("Compiling DSL RBI files...")
30
15
  say("")
31
16
 
@@ -30,6 +30,18 @@ module Tapioca
30
30
  end
31
31
  end
32
32
 
33
+ unless @skipped_gems.empty?
34
+ say("\nNote: Tapioca skipped RBI generation for the following gems because they are ignored by default:", [:yellow, :bold])
35
+ say(@skipped_gems.join(", "), [:yellow, :bold])
36
+ end
37
+
38
+ user_excluded_gems = @gem_names & @exclude
39
+
40
+ unless user_excluded_gems.empty?
41
+ say("\nNote: Tapioca skipped RBI generation for the following gems because they were excluded:", [:yellow, :bold])
42
+ say(user_excluded_gems.join(", "), [:yellow, :bold])
43
+ end
44
+
33
45
  if anything_done
34
46
  validate_rbi_files(
35
47
  command: default_command(:gem, @gem_names.join(" ")),
@@ -56,11 +68,15 @@ module Tapioca
56
68
  gem = @bundle.gem(gem_name)
57
69
 
58
70
  if gem.nil?
59
- next if @lsp_addon
60
-
61
- raise Tapioca::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red)
71
+ if @lsp_addon
72
+ next
73
+ elsif Gemfile::GemSpec::IGNORED_GEMS.include?(gem_name)
74
+ @skipped_gems << gem_name
75
+ next
76
+ else
77
+ raise Tapioca::Error, set_color("Error: Cannot find gem '#{gem_name}'", :red)
78
+ end
62
79
  end
63
-
64
80
  gems.concat(gem_dependencies(gem)) if @include_dependencies
65
81
  gems << gem
66
82
  end
@@ -74,9 +74,21 @@ module Tapioca
74
74
  @all_modules ||= if @@requested_constants.any?
75
75
  @@requested_constants.grep(Module)
76
76
  else
77
- ObjectSpace.each_object(Module).to_a
77
+ ObjectSpace.each_object(Module).reject { |mod| deprecated_constant_proxy?(mod) }.to_a
78
78
  end.freeze #: Enumerable[Module[top]]?
79
79
  end
80
+
81
+ # Rails 8.1+ wraps deprecated constants in DeprecatedConstantProxy, which
82
+ # undefines most instance methods and warns from method_missing. Inspecting
83
+ # those modules during DSL discovery (is_a?, singleton_class, etc.) emits
84
+ # deprecation warnings even though Tapioca is only enumerating ObjectSpace.
85
+ # class_of uses Kernel#class so we can recognize the proxy without warning.
86
+ #: (Module[top] mod) -> bool
87
+ def deprecated_constant_proxy?(mod)
88
+ proxy_class_name = name_of(class_of(mod))
89
+ proxy_class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy" ||
90
+ proxy_class_name == "ActiveSupport::DeprecatedConstantProxy"
91
+ end
80
92
  end
81
93
 
82
94
  #: (
@@ -163,25 +175,27 @@ module Tapioca
163
175
  parameters.each_with_index.map do |(type, name), index|
164
176
  fallback_arg_name = "_arg#{index}"
165
177
 
166
- name = name ? name.to_s : fallback_arg_name
167
- name = fallback_arg_name unless valid_parameter_name?(name)
178
+ sig_name = name ? name.to_s : fallback_arg_name
179
+ is_anonymous_parameter = anonymous_parameter_name?(type, sig_name)
180
+ sig_name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(sig_name)
181
+ param_name = is_anonymous_parameter ? nil : sig_name
168
182
  method_type = T.must(method_types[index])
169
183
 
170
184
  case type
171
185
  when :req
172
- create_param(name, type: method_type)
186
+ create_param(sig_name, type: method_type)
173
187
  when :opt
174
- create_opt_param(name, type: method_type, default: "T.unsafe(nil)")
188
+ create_opt_param(sig_name, type: method_type, default: "T.unsafe(nil)")
175
189
  when :rest
176
- create_rest_param(name, type: method_type)
190
+ create_rest_param(param_name, type: method_type)
177
191
  when :keyreq
178
- create_kw_param(name, type: method_type)
192
+ create_kw_param(sig_name, type: method_type)
179
193
  when :key
180
- create_kw_opt_param(name, type: method_type, default: "T.unsafe(nil)")
194
+ create_kw_opt_param(sig_name, type: method_type, default: "T.unsafe(nil)")
181
195
  when :keyrest
182
- create_kw_rest_param(name, type: method_type)
196
+ create_kw_rest_param(param_name, type: method_type)
183
197
  when :block
184
- create_block_param(name, type: method_type)
198
+ create_block_param(param_name, type: method_type)
185
199
  else
186
200
  raise "Unknown type `#{type}`."
187
201
  end
@@ -10,18 +10,20 @@ module Tapioca
10
10
  # This gem dynamically defines constants and methods at runtime. For
11
11
  # example, given a class:
12
12
  #
13
- # class MyClass
14
- # include AASM
13
+ # ~~~rb
14
+ # class MyClass
15
+ # include AASM
15
16
  #
16
- # aasm do
17
- # state :sleeping, initial: true
18
- # state :running, :cleaning
17
+ # aasm do
18
+ # state :sleeping, initial: true
19
+ # state :running, :cleaning
19
20
  #
20
- # event :run do
21
- # transitions from: :sleeping, to: :running
22
- # end
21
+ # event :run do
22
+ # transitions from: :sleeping, to: :running
23
23
  # end
24
24
  # end
25
+ # end
26
+ # ~~~
25
27
  #
26
28
  # This will result in the following constants being defined:
27
29
  #
@@ -94,14 +96,19 @@ module Tapioca
94
96
  create_rest_param("opts", type: "T.untyped"),
95
97
  create_block_param("block", type: "T.nilable(T.proc.void)"),
96
98
  ]
99
+ aasm_6_or_newer = ::Gem::Requirement.new(">= 6").satisfied_by?(::Gem::Version.new(::AASM::VERSION))
100
+
97
101
  state_machine.events.each do |event|
98
- model.create_method(event.name.to_s, parameters: parameters)
99
- model.create_method("#{event.name}!", parameters: parameters)
100
- model.create_method("#{event.name}_without_validation!", parameters: parameters)
101
- model.create_method("may_#{event.name}?", return_type: "T::Boolean")
102
+ # As of aasm 6.0 a namespaced state machine defines only the
103
+ # namespaced event methods (e.g. `run_foo!`) and not the plain
104
+ # methods (`run!`) as aliases.
105
+ unless namespace && aasm_6_or_newer
106
+ model.create_method(event.name.to_s, parameters: parameters)
107
+ model.create_method("#{event.name}!", parameters: parameters)
108
+ model.create_method("#{event.name}_without_validation!", parameters: parameters)
109
+ model.create_method("may_#{event.name}?", return_type: "T::Boolean")
110
+ end
102
111
 
103
- # For events, if there's a namespace the default methods are created in addition to
104
- # namespaced ones.
105
112
  next unless namespace
106
113
 
107
114
  name = "#{event.name}_#{namespace}"
@@ -110,9 +117,9 @@ module Tapioca
110
117
  model.create_method("#{name}!", parameters: parameters)
111
118
  model.create_method("may_#{name}?", return_type: "T::Boolean")
112
119
 
113
- # There's no namespaced method created for `_without_validation`, so skip
114
- # defining a method for:
115
- # "#{name}_without_validation!"
120
+ # aasm < 6 does not define a namespaced `_without_validation!`
121
+ # method.
122
+ model.create_method("#{name}_without_validation!", parameters: parameters) if aasm_6_or_newer
116
123
  end
117
124
  end
118
125
 
@@ -43,7 +43,7 @@ module Tapioca
43
43
  # @override
44
44
  #: -> void
45
45
  def decorate
46
- return unless constant.instance_methods(false).include?(:perform)
46
+ return unless constant.method_defined?(:perform, false)
47
47
 
48
48
  root.create_path(constant) do |job|
49
49
  method = constant.instance_method(:perform)
@@ -67,7 +67,7 @@ module Tapioca
67
67
 
68
68
  private
69
69
 
70
- HANDLED_METHOD_TARGETS = ["attribute", "attribute="] #: Array[String]
70
+ HANDLED_METHOD_TARGETS = ["attribute", "attribute="].freeze #: Array[String]
71
71
 
72
72
  #: -> Array[[::String, ::String]]
73
73
  def attribute_methods_for_constant