tapioca 0.19.1 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -0
  3. data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
  4. data/lib/tapioca/cli.rb +9 -0
  5. data/lib/tapioca/commands/abstract_dsl.rb +76 -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/gem_generate.rb +20 -4
  9. data/lib/tapioca/dsl/compiler.rb +24 -10
  10. data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
  11. data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
  12. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +64 -16
  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 +10 -3
  22. data/lib/tapioca/dsl/pipeline.rb +6 -27
  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 +2 -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 +91 -25
  40. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
  41. data/lib/tapioca/runtime/generic_type_registry.rb +38 -14
  42. data/lib/tapioca/runtime/reflection.rb +18 -1
  43. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -20
  44. data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
  45. data/lib/tapioca/version.rb +1 -1
  46. data/lib/tapioca.rb +11 -10
  47. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d3606b2440f453c78a226d8beec0ce61673d416fa74dba7d0380cd31404feaa
4
- data.tar.gz: c290cccb23e39c3423913357b187d74de382de4c818194676b206e0981f21c17
3
+ metadata.gz: 16f43e72a4dc002c9e6dcfa922274e3aff4de57318d48774eb7592ca4d548d8e
4
+ data.tar.gz: '03464479e0edf22b0abfe21536b674fd9000d2e7bd980585b4df01ab17f176e9'
5
5
  SHA512:
6
- metadata.gz: 4d655006a08405d41158b918625eadbcf7eec8b43da4c9077a7a9fe3ad94ee549865c27f0c66ae1fc0424afc8e6f30806e60f2ed661067264d920e52c6f04b70
7
- data.tar.gz: 96c0d237d2fda54650e919d74fc838b5121b714dcb3a4d35d60836911895265b99b29848f399b7f529050d4c930c3669e52ca3787dfbf1671b883883f39b6bba
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)
@@ -50,6 +51,8 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
50
51
  * [Using DSL compiler options](#using-dsl-compiler-options)
51
52
  * [Writing custom DSL compilers](#writing-custom-dsl-compilers)
52
53
  * [Writing custom DSL extensions](#writing-custom-dsl-extensions)
54
+ * [Rewriting RBS comments to Sorbet signatures](#rewriting-rbs-comments-to-sorbet-signatures)
55
+ * [Caching rewrites with Bootsnap](#caching-rewrites-with-bootsnap)
53
56
  * [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
54
57
  * [Configuration](#configuration)
55
58
  * [Editor Integration](#editor-integration)
@@ -343,6 +346,55 @@ This option can be used in CI to make sure the RBI files are *up-to-date* and en
343
346
 
344
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.
345
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
+
346
398
  ### Pulling RBI annotations from remote sources
347
399
 
348
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.
@@ -502,6 +554,8 @@ Options:
502
554
  -w, [--workers=N] # Number of parallel workers to use when generating RBIs (default: auto)
503
555
  [--rbi-max-line-length=N] # Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped
504
556
  # Default: 120
557
+ [--max-diff-lines=N] # Max number of diff lines to include in the `dsl --verify` output
558
+ # Default: 250
505
559
  -e, [--environment=ENVIRONMENT] # The Rack/Rails environment to use when generating RBIs
506
560
  # Default: development
507
561
  -l, [--list-compilers], [--no-list-compilers], [--skip-list-compilers] # List all loaded compilers
@@ -836,6 +890,35 @@ In order for DSL extensions to be discovered by Tapioca, they either needs to be
836
890
 
837
891
  For more concrete and advanced examples, take a look at [Tapioca's default DSL extensions](https://github.com/Shopify/tapioca/tree/main/lib/tapioca/dsl/extensions).
838
892
 
893
+ ### Rewriting RBS comments to Sorbet signatures
894
+
895
+ Tapioca translates [RBS comments](https://sorbet.org/docs/rbs-comments) into Sorbet `sig {}` blocks at file load time, so `sorbet-runtime` wraps the methods as if they had been written with native sigs. This is what lets the DSL command introspect signatures that were originally documented as RBS comments.
896
+
897
+ The rewriting is automatic on every `tapioca` invocation: [`require-hooks`](https://github.com/Shopify/require-hooks) intercepts `.rb` loads and `Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs` translates the source before Ruby compiles it to bytecode.
898
+
899
+ #### Caching rewrites with Bootsnap
900
+
901
+ `tapioca dsl` boots the app and eager-loads source files for introspection, so the rewrite runs across the whole codebase. On large applications this adds noticeable overhead. To cache the rewrite output across runs using [bootsnap](https://github.com/Shopify/bootsnap)'s iseq cache, you can set `TAPIOCA_RBS_CACHE=1`:
902
+
903
+ ```shell
904
+ $ TAPIOCA_RBS_CACHE=1 bin/tapioca dsl
905
+ ```
906
+
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.
914
+
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`:
916
+
917
+ ```ruby
918
+ # e.g. config/boot.rb
919
+ require "bootsnap/setup" unless ENV["TAPIOCA_RBS_CACHE"] == "1"
920
+ ```
921
+
839
922
  ### RBI files for missing constants and methods
840
923
 
841
924
  Even after generating the RBIs, it is possible that some constants or methods are still undefined for Sorbet.
@@ -960,6 +1043,7 @@ dsl:
960
1043
  quiet: false
961
1044
  workers: 1
962
1045
  rbi_max_line_length: 120
1046
+ max_diff_lines: 250
963
1047
  environment: development
964
1048
  list_compilers: false
965
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
@@ -116,6 +116,10 @@ module Tapioca
116
116
  type: :numeric,
117
117
  desc: "Set the max line length of generated RBIs. Signatures longer than the max line length will be wrapped",
118
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
119
123
  option :environment,
120
124
  aliases: ["-e"],
121
125
  type: :string,
@@ -149,6 +153,10 @@ module Tapioca
149
153
  # Assume anything starting with a capital letter or colon is a class, otherwise a path
150
154
  constants, paths = constant_or_paths.partition { |c| c =~ /\A[A-Z:]/ }
151
155
 
156
+ unless options[:max_diff_lines].positive?
157
+ raise MalformattedArgumentError, "Option '--max-diff-lines' must be a positive number"
158
+ end
159
+
152
160
  command_args = {
153
161
  requested_constants: constants,
154
162
  requested_paths: paths.map { |p| Pathname.new(p) },
@@ -166,6 +174,7 @@ module Tapioca
166
174
  halt_upon_load_error: options[:halt_upon_load_error],
167
175
  compiler_options: options[:compiler_options],
168
176
  lsp_addon: self.class.addon_mode,
177
+ max_diff_lines: options[:max_diff_lines],
169
178
  }
170
179
 
171
180
  command = if options[:verify]
@@ -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
@@ -95,6 +99,11 @@ module Tapioca
95
99
  )
96
100
  end.compact
97
101
 
102
+ if @only.any?
103
+ say("Skipping stale RBI removal because `--only` is set.", :yellow)
104
+ return Set.new
105
+ end
106
+
98
107
  files_to_purge = existing_rbi_files - generated_files
99
108
 
100
109
  files_to_purge
@@ -242,7 +251,7 @@ module Tapioca
242
251
  def perform_dsl_verification(dir)
243
252
  diff = verify_dsl_rbi(tmp_dir: dir)
244
253
 
245
- 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)
246
255
  ensure
247
256
  FileUtils.remove_entry(dir)
248
257
  end
@@ -264,7 +273,7 @@ module Tapioca
264
273
  @outpath / "#{underscore(constant_name)}.rbi"
265
274
  end
266
275
 
267
- #: (tmp_dir: Pathname) -> Hash[String, Symbol]
276
+ #: (tmp_dir: Pathname) -> Hash[Pathname, Symbol]
268
277
  def verify_dsl_rbi(tmp_dir:)
269
278
  diff = {}
270
279
 
@@ -296,7 +305,7 @@ module Tapioca
296
305
  diff
297
306
  end
298
307
 
299
- #: (Symbol cause, Array[String] files) -> String
308
+ #: (Symbol cause, Array[Pathname] files) -> String
300
309
  def build_error_for_files(cause, files)
301
310
  filenames = files.map do |file|
302
311
  @outpath / file
@@ -305,26 +314,73 @@ module Tapioca
305
314
  " File(s) #{cause}:\n - #{filenames}"
306
315
  end
307
316
 
308
- #: (Hash[String, Symbol] diff, Symbol command) -> void
309
- 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:)
310
319
  if diff.empty?
311
320
  say("Nothing to do, all RBIs are up-to-date.")
312
- else
313
- reasons = diff.group_by(&:last).sort.map do |cause, diff_for_cause|
314
- build_error_for_files(cause, diff_for_cause.map(&:first))
315
- 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
316
338
 
317
- raise Tapioca::Error, <<~ERROR
318
- #{set_color("RBI files are out-of-date. In your development environment, please run:", :green)}
319
- #{set_color("`#{default_command(command)}`", :green, :bold)}
320
- #{set_color("Once it is complete, be sure to commit and push any changes", :green)}
321
- If you don't observe any changes after running the command locally, ensure your database is in a good
322
- state e.g. run `bin/rails db:reset`
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`
323
345
 
324
- #{set_color("Reason:", :red)}
325
- #{reasons}
326
- ERROR
346
+ #{set_color("Reason:", :red)}
347
+ #{reasons}
348
+
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
327
381
  end
382
+
383
+ [out, truncated]
328
384
  end
329
385
 
330
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
@@ -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