sorbet-runtime 0.4.4667 → 0.5.6189

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/lib/sorbet-runtime.rb +15 -3
  3. data/lib/types/_types.rb +26 -17
  4. data/lib/types/boolean.rb +1 -1
  5. data/lib/types/compatibility_patches.rb +65 -10
  6. data/lib/types/configuration.rb +93 -7
  7. data/lib/types/enum.rb +371 -0
  8. data/lib/types/generic.rb +2 -2
  9. data/lib/types/interface_wrapper.rb +4 -4
  10. data/lib/types/non_forcing_constants.rb +61 -0
  11. data/lib/types/private/abstract/data.rb +2 -2
  12. data/lib/types/private/abstract/declare.rb +3 -0
  13. data/lib/types/private/abstract/validate.rb +7 -7
  14. data/lib/types/private/casts.rb +27 -0
  15. data/lib/types/private/class_utils.rb +8 -5
  16. data/lib/types/private/methods/_methods.rb +80 -28
  17. data/lib/types/private/methods/call_validation.rb +5 -47
  18. data/lib/types/private/methods/decl_builder.rb +14 -56
  19. data/lib/types/private/methods/modes.rb +5 -7
  20. data/lib/types/private/methods/signature.rb +32 -18
  21. data/lib/types/private/methods/signature_validation.rb +29 -35
  22. data/lib/types/private/retry.rb +10 -0
  23. data/lib/types/private/sealed.rb +21 -1
  24. data/lib/types/private/types/type_alias.rb +31 -0
  25. data/lib/types/private/types/void.rb +4 -3
  26. data/lib/types/profile.rb +5 -1
  27. data/lib/types/props/_props.rb +3 -7
  28. data/lib/types/props/constructor.rb +29 -9
  29. data/lib/types/props/custom_type.rb +51 -27
  30. data/lib/types/props/decorator.rb +248 -405
  31. data/lib/types/props/generated_code_validation.rb +268 -0
  32. data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
  33. data/lib/types/props/optional.rb +37 -41
  34. data/lib/types/props/plugin.rb +23 -1
  35. data/lib/types/props/pretty_printable.rb +3 -3
  36. data/lib/types/props/private/apply_default.rb +170 -0
  37. data/lib/types/props/private/deserializer_generator.rb +165 -0
  38. data/lib/types/props/private/parser.rb +32 -0
  39. data/lib/types/props/private/serde_transform.rb +186 -0
  40. data/lib/types/props/private/serializer_generator.rb +77 -0
  41. data/lib/types/props/private/setter_factory.rb +139 -0
  42. data/lib/types/props/serializable.rb +137 -192
  43. data/lib/types/props/type_validation.rb +19 -6
  44. data/lib/types/props/utils.rb +3 -7
  45. data/lib/types/props/weak_constructor.rb +51 -14
  46. data/lib/types/sig.rb +6 -6
  47. data/lib/types/types/attached_class.rb +37 -0
  48. data/lib/types/types/base.rb +26 -2
  49. data/lib/types/types/fixed_array.rb +28 -2
  50. data/lib/types/types/fixed_hash.rb +11 -10
  51. data/lib/types/types/intersection.rb +6 -0
  52. data/lib/types/types/noreturn.rb +4 -0
  53. data/lib/types/types/self_type.rb +4 -0
  54. data/lib/types/types/simple.rb +22 -1
  55. data/lib/types/types/t_enum.rb +38 -0
  56. data/lib/types/types/type_parameter.rb +1 -1
  57. data/lib/types/types/type_variable.rb +1 -1
  58. data/lib/types/types/typed_array.rb +7 -2
  59. data/lib/types/types/typed_enumerable.rb +28 -17
  60. data/lib/types/types/typed_enumerator.rb +7 -2
  61. data/lib/types/types/typed_hash.rb +8 -3
  62. data/lib/types/types/typed_range.rb +7 -2
  63. data/lib/types/types/typed_set.rb +7 -2
  64. data/lib/types/types/union.rb +37 -5
  65. data/lib/types/types/untyped.rb +4 -0
  66. data/lib/types/utils.rb +43 -11
  67. metadata +103 -11
  68. data/lib/types/private/error_handler.rb +0 -0
  69. data/lib/types/runtime_profiled.rb +0 -24
  70. data/lib/types/types/opus_enum.rb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7082fbca451e3fa884be8384d3eb5d2d481dd246
4
- data.tar.gz: 5cd6efee114d05e0bcfb594f0d9cf13a11138ba6
2
+ SHA256:
3
+ metadata.gz: '078455e4f5dfee05efee1b1dc5283a572d80e4583699ef18fbd51cc606c1a559'
4
+ data.tar.gz: 6d77cb7b423ae756a33c04e15a50ff0d7d17a0cbd0e25460c0ce89b25959fa97
5
5
  SHA512:
6
- metadata.gz: d0afdd063bf1a2b95dc4c725c088452e41b4ecb7c1dd016fecbcda5bd707effaf474fff64ae814bb99a92372b504912fede7167a0faa21375a0002a12906852d
7
- data.tar.gz: 7ac4c1933ab8661790c78890181594f89e2d31482c9217d532f4f9087741e29a03a086031307218bb5898ab58f544fb5bdeff0df7b2adf6aa7c68a741d46b3b1
6
+ metadata.gz: 171c564c0b615d126fa8e14bff2dc5567baa53a2c0d702032f8971fd4f83e1cd3c3d888e4c4ac0e9843e0b516fe1563807cd6d8afcb9d8c0dfa15da28ff8758d
7
+ data.tar.gz: 967a1bb33ed88e50c31bf01dbbfdaad43e7ef7f66723c5648a58bc47d19cb5f5c46b7fd9a3733310cc07eb72743d32bb80341aa7e3f529ea5584def01195591e
@@ -22,9 +22,7 @@ require_relative 'types/configuration'
22
22
  require_relative 'types/profile'
23
23
  require_relative 'types/_types'
24
24
  require_relative 'types/private/decl_state'
25
- require_relative 'types/runtime_profiled'
26
25
  require_relative 'types/private/class_utils'
27
- require_relative 'types/private/error_handler'
28
26
  require_relative 'types/private/runtime_levels'
29
27
  require_relative 'types/private/methods/_methods'
30
28
  require_relative 'types/sig'
@@ -43,9 +41,10 @@ require_relative 'types/types/fixed_hash'
43
41
  require_relative 'types/types/intersection'
44
42
  require_relative 'types/types/noreturn'
45
43
  require_relative 'types/types/proc'
44
+ require_relative 'types/types/attached_class'
46
45
  require_relative 'types/types/self_type'
47
46
  require_relative 'types/types/simple'
48
- require_relative 'types/types/opus_enum'
47
+ require_relative 'types/types/t_enum'
49
48
  require_relative 'types/types/type_parameter'
50
49
  require_relative 'types/types/typed_array'
51
50
  require_relative 'types/types/typed_enumerator'
@@ -57,6 +56,7 @@ require_relative 'types/types/untyped'
57
56
  require_relative 'types/private/types/not_typed'
58
57
  require_relative 'types/private/types/void'
59
58
  require_relative 'types/private/types/string_holder'
59
+ require_relative 'types/private/types/type_alias'
60
60
 
61
61
  require_relative 'types/types/type_variable'
62
62
  require_relative 'types/types/type_member'
@@ -79,6 +79,7 @@ require_relative 'types/private/abstract/hooks'
79
79
  require_relative 'types/private/casts'
80
80
  require_relative 'types/private/methods/decl_builder'
81
81
  require_relative 'types/private/methods/signature'
82
+ require_relative 'types/private/retry'
82
83
  require_relative 'types/utils'
83
84
  require_relative 'types/boolean'
84
85
 
@@ -91,13 +92,24 @@ require_relative 'types/props/decorator'
91
92
  require_relative 'types/props/errors'
92
93
  require_relative 'types/props/plugin'
93
94
  require_relative 'types/props/utils'
95
+ require_relative 'types/enum'
94
96
  # Props that run sigs statically so have to be after all the others :(
97
+ require_relative 'types/props/private/setter_factory'
98
+ require_relative 'types/props/private/apply_default'
99
+ require_relative 'types/props/has_lazily_specialized_methods'
95
100
  require_relative 'types/props/optional'
96
101
  require_relative 'types/props/weak_constructor'
97
102
  require_relative 'types/props/constructor'
98
103
  require_relative 'types/props/pretty_printable'
104
+ require_relative 'types/props/private/serde_transform'
105
+ require_relative 'types/props/private/deserializer_generator'
106
+ require_relative 'types/props/private/serializer_generator'
99
107
  require_relative 'types/props/serializable'
100
108
  require_relative 'types/props/type_validation'
109
+ require_relative 'types/props/private/parser'
110
+ require_relative 'types/props/generated_code_validation'
111
+
101
112
  require_relative 'types/struct'
113
+ require_relative 'types/non_forcing_constants'
102
114
 
103
115
  require_relative 'types/compatibility_patches'
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
3
  # This is where we define the shortcuts, so we can't use them here
4
- # rubocop:disable PrisonGuard/UseOpusTypesShortcut
5
4
 
6
5
  # _____
7
6
  # |_ _| _ _ __ ___ ___
@@ -26,23 +25,26 @@
26
25
  module T
27
26
  # T.any(<Type>, <Type>, ...) -- matches any of the types listed
28
27
  def self.any(type_a, type_b, *types)
29
- T::Types::Union.new([type_a, type_b] + types)
28
+ type_a = T::Utils.coerce(type_a)
29
+ type_b = T::Utils.coerce(type_b)
30
+ types = types.map {|t| T::Utils.coerce(t)} if !types.empty?
31
+ T::Types::Union::Private::Pool.union_of_types(type_a, type_b, types)
30
32
  end
31
33
 
32
34
  # Shorthand for T.any(type, NilClass)
33
35
  def self.nilable(type)
34
- T::Types::Union.new([type, NilClass])
36
+ T::Types::Union::Private::Pool.union_of_types(T::Utils.coerce(type), T::Utils::Nilable::NIL_TYPE)
35
37
  end
36
38
 
37
39
  # Matches any object. In the static checker, T.untyped allows any
38
40
  # method calls or operations.
39
41
  def self.untyped
40
- T::Types::Untyped.new
42
+ T::Types::Untyped::Private::INSTANCE
41
43
  end
42
44
 
43
45
  # Indicates a function never returns (e.g. "Kernel#raise")
44
46
  def self.noreturn
45
- T::Types::NoReturn.new
47
+ T::Types::NoReturn::Private::INSTANCE
46
48
  end
47
49
 
48
50
  # T.all(<Type>, <Type>, ...) -- matches an object that has all of the types listed
@@ -62,7 +64,12 @@ module T
62
64
 
63
65
  # Matches `self`:
64
66
  def self.self_type
65
- T::Types::SelfType.new
67
+ T::Types::SelfType::Private::INSTANCE
68
+ end
69
+
70
+ # Matches the instance type in a singleton-class context
71
+ def self.attached_class
72
+ T::Types::AttachedClassType::Private::INSTANCE
66
73
  end
67
74
 
68
75
  # Matches any class that subclasses or includes the provided class
@@ -75,11 +82,11 @@ module T
75
82
  ## END OF THE METHODS TO PASS TO `sig`.
76
83
 
77
84
 
78
- # Constructs a type alias. Used to create a short name for a larger
79
- # type. In Ruby this is just equivalent to assignment, but this is
80
- # needed for support by the static checker. Example usage:
85
+ # Constructs a type alias. Used to create a short name for a larger type. In Ruby this returns a
86
+ # wrapper that contains a proc that is evaluated to get the underlying type. This syntax however
87
+ # is needed for support by the static checker. Example usage:
81
88
  #
82
- # NilableString = T.type_alias(T.nilable(String))
89
+ # NilableString = T.type_alias {T.nilable(String)}
83
90
  #
84
91
  # sig {params(arg: NilableString, default: String).returns(String)}
85
92
  # def or_else(arg, default)
@@ -88,11 +95,17 @@ module T
88
95
  #
89
96
  # The name of the type alias is not preserved; Error messages will
90
97
  # be printed with reference to the underlying type.
91
- def self.type_alias(type)
92
- T::Utils.coerce(type)
98
+ #
99
+ # TODO Remove `type` parameter. This was left in to make life easier while migrating.
100
+ def self.type_alias(type=nil, &blk)
101
+ if blk
102
+ T::Private::Types::TypeAlias.new(blk)
103
+ else
104
+ T::Utils.coerce(type)
105
+ end
93
106
  end
94
107
 
95
- # References a type paramater which was previously defined with
108
+ # References a type parameter which was previously defined with
96
109
  # `type_parameters`.
97
110
  #
98
111
  # This is used for generic methods. Example usage:
@@ -270,8 +283,4 @@ module T
270
283
  end
271
284
  end
272
285
  end
273
-
274
- # When mixed into a module, indicates that Sorbet may export the CFG for methods in that module
275
- module CFGExport; end
276
286
  end
277
- # rubocop:enable PrisonGuard/UseOpusTypesShortcut
@@ -4,5 +4,5 @@
4
4
  module T
5
5
  # T::Boolean is a type alias helper for the common `T.any(TrueClass, FalseClass)`.
6
6
  # Defined separately from _types.rb because it has a dependency on T::Types::Union.
7
- Boolean = T.type_alias(T.any(TrueClass, FalseClass))
7
+ Boolean = T.type_alias {T.any(TrueClass, FalseClass)}
8
8
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: ignore
3
3
 
4
- require_relative 'private/methods/_methods'
5
-
6
4
  # Work around an interaction bug with sorbet-runtime and rspec-mocks,
7
- # which occurs when using *_any_instance_of and and_call_original.
5
+ # which occurs when using message expectations (*_any_instance_of,
6
+ # expect, allow) and and_call_original.
8
7
  #
9
8
  # When a sig is defined, sorbet-runtime will replace the sigged method
10
9
  # with a wrapper that, upon first invocation, re-wraps the method with a faster
@@ -22,17 +21,73 @@ require_relative 'private/methods/_methods'
22
21
  #
23
22
  # We work around this by forcing re-wrapping before rspec stores a reference
24
23
  # to the method.
25
- if defined? ::RSpec::Mocks::AnyInstance
24
+ if defined? ::RSpec::Mocks
26
25
  module T
27
26
  module CompatibilityPatches
28
- module RecorderExtensions
29
- def observe!(method_name)
30
- method = @klass.instance_method(method_name.to_sym)
31
- T::Private::Methods.maybe_run_sig_block_for_method(method)
32
- super(method_name)
27
+ module RSpecCompatibility
28
+ module RecorderExtensions
29
+ def observe!(method_name)
30
+ method = @klass.instance_method(method_name.to_sym)
31
+ T::Private::Methods.maybe_run_sig_block_for_method(method)
32
+ super(method_name)
33
+ end
34
+ end
35
+ ::RSpec::Mocks::AnyInstance::Recorder.prepend(RecorderExtensions) if defined?(::RSpec::Mocks::AnyInstance::Recorder)
36
+
37
+ module MethodDoubleExtensions
38
+ def initialize(object, method_name, proxy)
39
+ if ::Kernel.instance_method(:respond_to?).bind(object).call(method_name, true)
40
+ method = ::RSpec::Support.method_handle_for(object, method_name)
41
+ T::Private::Methods.maybe_run_sig_block_for_method(method)
42
+ end
43
+ super(object, method_name, proxy)
44
+ end
33
45
  end
46
+ ::RSpec::Mocks::MethodDouble.prepend(MethodDoubleExtensions) if defined?(::RSpec::Mocks::MethodDouble)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Work around for sorbet-runtime wrapped methods.
53
+ #
54
+ # When a sig is defined, sorbet-runtime will replace the sigged method
55
+ # with a wrapper. Those wrapper methods look like `foo(*args, &blk)`
56
+ # so that wrappers can handle and pass on all the arguments supplied.
57
+ #
58
+ # However, that creates a problem with runtime reflection on the methods,
59
+ # since when a sigged method is introspected, it will always return its
60
+ # `arity` as `-1`, its `parameters` as `[[:rest, :args], [:block, :blk]]`,
61
+ # and its `source_location` as `[<some_file_in_sorbet>, <some_line_number>]`.
62
+ #
63
+ # This might be a problem for some applications that rely on getting the
64
+ # correct information from these methods.
65
+ #
66
+ # This compatibility module, when prepended to the `Method` class, would fix
67
+ # the return values of `arity`, `parameters` and `source_location`.
68
+ #
69
+ # @example
70
+ # require 'sorbet-runtime'
71
+ # ::Method.prepend(T::CompatibilityPatches::MethodExtensions)
72
+ module T
73
+ module CompatibilityPatches
74
+ module MethodExtensions
75
+ def arity
76
+ arity = super
77
+ return arity if arity != -1 || self.is_a?(Proc)
78
+ sig = T::Private::Methods.signature_for_method(self)
79
+ sig ? sig.method.arity : arity
80
+ end
81
+
82
+ def source_location
83
+ sig = T::Private::Methods.signature_for_method(self)
84
+ sig ? sig.method.source_location : super
85
+ end
86
+
87
+ def parameters
88
+ sig = T::Private::Methods.signature_for_method(self)
89
+ sig ? sig.method.parameters : super
34
90
  end
35
- ::RSpec::Mocks::AnyInstance::Recorder.prepend(RecorderExtensions)
36
91
  end
37
92
  end
38
93
  end
@@ -42,6 +42,34 @@ module T::Configuration
42
42
  T::Private::Methods.set_final_checks_on_hooks(false)
43
43
  end
44
44
 
45
+ @include_value_in_type_errors = true
46
+ # Whether to include values in TypeError messages.
47
+ #
48
+ # Including values is useful for debugging, but can potentially leak
49
+ # sensitive information to logs.
50
+ #
51
+ # @return [T::Boolean]
52
+ def self.include_value_in_type_errors?
53
+ @include_value_in_type_errors
54
+ end
55
+
56
+ # Configure if type errors excludes the value of the problematic type.
57
+ #
58
+ # The default is to include values in type errors:
59
+ # TypeError: Expected type Integer, got String with value "foo"
60
+ #
61
+ # When values are excluded from type errors:
62
+ # TypeError: Expected type Integer, got String
63
+ def self.exclude_value_in_type_errors
64
+ @include_value_in_type_errors = false
65
+ end
66
+
67
+ # Opposite of exclude_value_in_type_errors.
68
+ # (Including values in type errors is the default)
69
+ def self.include_value_in_type_errors
70
+ @include_value_in_type_errors = true
71
+ end
72
+
45
73
  # Configure the default checked level for a sig with no explicit `.checked`
46
74
  # builder. When unset, the default checked level is `:always`.
47
75
  #
@@ -248,7 +276,7 @@ module T::Configuration
248
276
  end
249
277
 
250
278
  private_class_method def self.log_info_handler_default(str, extra)
251
- puts "#{str}, extra: #{extra}" # rubocop:disable PrisonGuard/NoBarePuts
279
+ puts "#{str}, extra: #{extra}"
252
280
  end
253
281
 
254
282
  def self.log_info_handler(str, extra)
@@ -282,7 +310,7 @@ module T::Configuration
282
310
  end
283
311
 
284
312
  private_class_method def self.soft_assert_handler_default(str, extra)
285
- puts "#{str}, extra: #{extra}" # rubocop:disable PrisonGuard/NoBarePuts
313
+ puts "#{str}, extra: #{extra}"
286
314
  end
287
315
 
288
316
  def self.soft_assert_handler(str, extra)
@@ -319,7 +347,7 @@ module T::Configuration
319
347
  raise str
320
348
  end
321
349
 
322
- def self.hard_assert_handler(str, extra)
350
+ def self.hard_assert_handler(str, extra={})
323
351
  if @hard_assert_handler
324
352
  @hard_assert_handler.call(str, extra)
325
353
  else
@@ -336,9 +364,9 @@ module T::Configuration
336
364
  # T::Configuration.scalar_types = ["NilClass", "TrueClass", "FalseClass", ...]
337
365
  def self.scalar_types=(values)
338
366
  if values.nil?
339
- @scalar_tyeps = values
367
+ @scalar_types = values
340
368
  else
341
- bad_values = values.select {|v| v.class != String}
369
+ bad_values = values.reject {|v| v.class == String}
342
370
  unless bad_values.empty?
343
371
  raise ArgumentError.new("Provided values must all be class name strings.")
344
372
  end
@@ -347,7 +375,7 @@ module T::Configuration
347
375
  end
348
376
  end
349
377
 
350
- @default_scalar_types = Set.new(%w{
378
+ @default_scalar_types = Set.new(%w[
351
379
  NilClass
352
380
  TrueClass
353
381
  FalseClass
@@ -356,12 +384,34 @@ module T::Configuration
356
384
  String
357
385
  Symbol
358
386
  Time
359
- }).freeze
387
+ T::Enum
388
+ ]).freeze
360
389
 
361
390
  def self.scalar_types
362
391
  @scalar_types || @default_scalar_types
363
392
  end
364
393
 
394
+ # Guard against overrides of `name` or `to_s`
395
+ MODULE_NAME = Module.instance_method(:name)
396
+ private_constant :MODULE_NAME
397
+
398
+ @default_module_name_mangler = ->(type) {MODULE_NAME.bind(type).call}
399
+ @module_name_mangler = nil
400
+
401
+ def self.module_name_mangler
402
+ @module_name_mangler || @default_module_name_mangler
403
+ end
404
+
405
+ # Set to override the default behavior for converting types
406
+ # to names in generated code. Used by the runtime implementation
407
+ # associated with `--stripe-packages` mode.
408
+ #
409
+ # @param [Lambda, Proc, nil] value Proc that converts a type (Class/Module)
410
+ # to a String (pass nil to reset to default behavior)
411
+ def self.module_name_mangler=(handler)
412
+ @module_name_mangler = handler
413
+ end
414
+
365
415
  # Temporarily disable ruby warnings while executing the given block. This is
366
416
  # useful when doing something that would normally cause a warning to be
367
417
  # emitted in Ruby verbose mode ($VERBOSE = true).
@@ -382,6 +432,42 @@ module T::Configuration
382
432
  end
383
433
  end
384
434
 
435
+ def self.enable_legacy_t_enum_migration_mode
436
+ @legacy_t_enum_migration_mode = true
437
+ end
438
+ def self.disable_legacy_t_enum_migration_mode
439
+ @legacy_t_enum_migration_mode = false
440
+ end
441
+ def self.legacy_t_enum_migration_mode?
442
+ @legacy_t_enum_migration_mode || false
443
+ end
444
+
445
+ # @param [Array] sealed_violation_whitelist An array of Regexp to validate
446
+ # whether inheriting /including a sealed module outside the defining module
447
+ # should be allowed. Useful to whitelist benign violations, like shim files
448
+ # generated for an autoloader.
449
+ def self.sealed_violation_whitelist=(sealed_violation_whitelist)
450
+ if !@sealed_violation_whitelist.nil?
451
+ raise ArgumentError.new("Cannot overwrite sealed_violation_whitelist after setting it")
452
+ end
453
+
454
+ case sealed_violation_whitelist
455
+ when Array
456
+ sealed_violation_whitelist.each do |x|
457
+ case x
458
+ when Regexp then nil
459
+ else raise TypeError.new("sealed_violation_whitelist accepts an Array of Regexp")
460
+ end
461
+ end
462
+ else
463
+ raise TypeError.new("sealed_violation_whitelist= accepts an Array of Regexp")
464
+ end
465
+
466
+ @sealed_violation_whitelist = sealed_violation_whitelist
467
+ end
468
+ def self.sealed_violation_whitelist
469
+ @sealed_violation_whitelist
470
+ end
385
471
 
386
472
  private_class_method def self.validate_lambda_given!(value)
387
473
  if !value.nil? && !value.respond_to?(:call)
@@ -0,0 +1,371 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ # Enumerations allow for type-safe declarations of a fixed set of values.
5
+ #
6
+ # Every value is a singleton instance of the class (i.e. `Suit::SPADE.is_a?(Suit) == true`).
7
+ #
8
+ # Each value has a corresponding serialized value. By default this is the constant's name converted
9
+ # to lowercase (e.g. `Suit::Club.serialize == 'club'`); however a custom value may be passed to the
10
+ # constructor. Enum will `freeze` the serialized value.
11
+ #
12
+ # @example Declaring an Enum:
13
+ # class Suit < T::Enum
14
+ # enums do
15
+ # CLUB = new
16
+ # SPADE = new
17
+ # DIAMOND = new
18
+ # HEART = new
19
+ # end
20
+ # end
21
+ #
22
+ # @example Custom serialization value:
23
+ # class Status < T::Enum
24
+ # enums do
25
+ # READY = new('rdy')
26
+ # ...
27
+ # end
28
+ # end
29
+ #
30
+ # @example Accessing values:
31
+ # Suit::SPADE
32
+ #
33
+ # @example Converting from serialized value to enum instance:
34
+ # Suit.deserialize('club') == Suit::CLUB
35
+ #
36
+ # @example Using enums in type signatures:
37
+ # sig {params(suit: Suit).returns(Boolean)}
38
+ # def is_red?(suit); ...; end
39
+ #
40
+ # WARNING: Enum instances are singletons that are shared among all their users. Their internals
41
+ # should be kept immutable to avoid unpredictable action at a distance.
42
+ class T::Enum
43
+ extend T::Sig
44
+ extend T::Props::CustomType
45
+
46
+ # TODO(jez) Might want to restrict this, or make subclasses provide this type
47
+ SerializedVal = T.type_alias {T.untyped}
48
+ private_constant :SerializedVal
49
+
50
+ ## Enum class methods ##
51
+ sig {returns(T::Array[T.attached_class])}
52
+ def self.values
53
+ if @values.nil?
54
+ raise "Attempting to access values of #{self.class} before it has been initialized." \
55
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
56
+ end
57
+ @values
58
+ end
59
+
60
+ # This exists for compatibility with the interface of `Hash` & mostly to support
61
+ # the HashEachMethods Rubocop.
62
+ sig {params(blk: T.nilable(T.proc.params(arg0: T.attached_class).void)).returns(T.any(T::Enumerator[T.attached_class], T::Array[T.attached_class]))}
63
+ def self.each_value(&blk)
64
+ if blk
65
+ values.each(&blk)
66
+ else
67
+ values.each
68
+ end
69
+ end
70
+
71
+ # Convert from serialized value to enum instance
72
+ #
73
+ # Note: It would have been nice to make this method final before people started overriding it.
74
+ # Note: Failed CriticalMethodsNoRuntimeTypingTest
75
+ sig {params(serialized_val: SerializedVal).returns(T.nilable(T.attached_class)).checked(:never)}
76
+ def self.try_deserialize(serialized_val)
77
+ if @mapping.nil?
78
+ raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
79
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
80
+ end
81
+ @mapping[serialized_val]
82
+ end
83
+
84
+ # Convert from serialized value to enum instance.
85
+ #
86
+ # Note: It would have been nice to make this method final before people started overriding it.
87
+ # Note: Failed CriticalMethodsNoRuntimeTypingTest
88
+ #
89
+ # @return [self]
90
+ # @raise [KeyError] if serialized value does not match any instance.
91
+ sig {overridable.params(serialized_val: SerializedVal).returns(T.attached_class).checked(:never)}
92
+ def self.from_serialized(serialized_val)
93
+ res = try_deserialize(serialized_val)
94
+ if res.nil?
95
+ raise KeyError.new("Enum #{self} key not found: #{serialized_val.inspect}")
96
+ end
97
+ res
98
+ end
99
+
100
+ # Note: It would have been nice to make this method final before people started overriding it.
101
+ # @return [Boolean] Does the given serialized value correspond with any of this enum's values.
102
+ sig {overridable.params(serialized_val: SerializedVal).returns(T::Boolean).checked(:never)}
103
+ def self.has_serialized?(serialized_val)
104
+ if @mapping.nil?
105
+ raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
106
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
107
+ end
108
+ @mapping.include?(serialized_val)
109
+ end
110
+
111
+ # Note: Failed CriticalMethodsNoRuntimeTypingTest
112
+ sig {override.params(instance: T.nilable(T::Enum)).returns(SerializedVal).checked(:never)}
113
+ def self.serialize(instance)
114
+ # This is needed otherwise if a Chalk::ODM::Document with a property of the shape
115
+ # T::Hash[T.nilable(MyEnum), Integer] and a value that looks like {nil => 0} is
116
+ # serialized, we throw the error on L102.
117
+ return nil if instance.nil?
118
+
119
+ if self == T::Enum
120
+ raise "Cannot call T::Enum.serialize directly. You must call on a specific child class."
121
+ end
122
+ if instance.class != self
123
+ raise "Cannot call #serialize on a value that is not an instance of #{self}."
124
+ end
125
+ instance.serialize
126
+ end
127
+
128
+ # Note: Failed CriticalMethodsNoRuntimeTypingTest
129
+ sig {override.params(mongo_value: SerializedVal).returns(T.attached_class).checked(:never)}
130
+ def self.deserialize(mongo_value)
131
+ if self == T::Enum
132
+ raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
133
+ end
134
+ self.from_serialized(mongo_value)
135
+ end
136
+
137
+
138
+ ## Enum instance methods ##
139
+
140
+
141
+ sig {returns(T.self_type)}
142
+ def dup
143
+ self
144
+ end
145
+
146
+ sig {returns(T.self_type).checked(:tests)}
147
+ def clone
148
+ self
149
+ end
150
+
151
+ # Note: Failed CriticalMethodsNoRuntimeTypingTest
152
+ sig {returns(SerializedVal).checked(:never)}
153
+ def serialize
154
+ assert_bound!
155
+ @serialized_val
156
+ end
157
+
158
+ sig {params(args: T.untyped).returns(T.untyped)}
159
+ def to_json(*args)
160
+ serialize.to_json(*args)
161
+ end
162
+
163
+ sig {returns(String)}
164
+ def to_s
165
+ inspect
166
+ end
167
+
168
+ sig {returns(String)}
169
+ def inspect
170
+ "#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
171
+ end
172
+
173
+ sig {params(other: BasicObject).returns(T.nilable(Integer))}
174
+ def <=>(other)
175
+ case other
176
+ when self.class
177
+ self.serialize <=> other.serialize
178
+ else
179
+ nil
180
+ end
181
+ end
182
+
183
+
184
+ # NB: Do not call this method. This exists to allow for a safe migration path in places where enum
185
+ # values are compared directly against string values.
186
+ #
187
+ # Ruby's string has a weird quirk where `'my_string' == obj` calls obj.==('my_string') if obj
188
+ # responds to the `to_str` method. It does not actually call `to_str` however.
189
+ #
190
+ # See https://ruby-doc.org/core-2.4.0/String.html#method-i-3D-3D
191
+ sig {returns(String)}
192
+ def to_str
193
+ msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
194
+ if T::Configuration.legacy_t_enum_migration_mode?
195
+ T::Configuration.soft_assert_handler(
196
+ msg,
197
+ storytime: {class: self.class.name},
198
+ )
199
+ serialize.to_s
200
+ else
201
+ raise NoMethodError.new(msg)
202
+ end
203
+ end
204
+
205
+ sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
206
+ def ==(other)
207
+ case other
208
+ when String
209
+ if T::Configuration.legacy_t_enum_migration_mode?
210
+ comparison_assertion_failed(:==, other)
211
+ self.serialize == other
212
+ else
213
+ false
214
+ end
215
+ else
216
+ super(other)
217
+ end
218
+ end
219
+
220
+ sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
221
+ def ===(other)
222
+ case other
223
+ when String
224
+ if T::Configuration.legacy_t_enum_migration_mode?
225
+ comparison_assertion_failed(:===, other)
226
+ self.serialize == other
227
+ else
228
+ false
229
+ end
230
+ else
231
+ super(other)
232
+ end
233
+ end
234
+
235
+ sig {params(method: Symbol, other: T.untyped).void}
236
+ private def comparison_assertion_failed(method, other)
237
+ T::Configuration.soft_assert_handler(
238
+ 'Enum to string comparison not allowed. Compare to the Enum instance directly instead. See go/enum-migration',
239
+ storytime: {
240
+ class: self.class.name,
241
+ self: self.inspect,
242
+ other: other,
243
+ other_class: other.class.name,
244
+ method: method,
245
+ }
246
+ )
247
+ end
248
+
249
+
250
+ ## Private implementation ##
251
+
252
+
253
+ sig {params(serialized_val: SerializedVal).void}
254
+ def initialize(serialized_val=nil)
255
+ raise 'T::Enum is abstract' if self.class == T::Enum
256
+ if !self.class.started_initializing?
257
+ raise "Must instantiate all enum values of #{self.class} inside 'enums do'."
258
+ end
259
+ if self.class.fully_initialized?
260
+ raise "Cannot instantiate a new enum value of #{self.class} after it has been initialized."
261
+ end
262
+
263
+ serialized_val = serialized_val.frozen? ? serialized_val : serialized_val.dup.freeze
264
+ @serialized_val = T.let(serialized_val, T.nilable(SerializedVal))
265
+ @const_name = T.let(nil, T.nilable(Symbol))
266
+ self.class._register_instance(self)
267
+ end
268
+
269
+ sig {returns(NilClass).checked(:never)}
270
+ private def assert_bound!
271
+ if @const_name.nil?
272
+ raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
273
+ " Enums are not initialized until the 'enums do' block they are defined in has finished running."
274
+ end
275
+ end
276
+
277
+ sig {params(const_name: Symbol).void}
278
+ def _bind_name(const_name)
279
+ @const_name = const_name
280
+ @serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil?
281
+ freeze
282
+ end
283
+
284
+ sig {params(const_name: Symbol).returns(String)}
285
+ private def const_to_serialized_val(const_name)
286
+ # Historical note: We convert to lowercase names because the majority of existing calls to
287
+ # `make_accessible` were arrays of lowercase strings. Doing this conversion allowed for the
288
+ # least amount of repetition in migrated declarations.
289
+ const_name.to_s.downcase.freeze
290
+ end
291
+
292
+ sig {returns(T::Boolean)}
293
+ def self.started_initializing?
294
+ @started_initializing = T.let(@started_initializing, T.nilable(T::Boolean))
295
+ @started_initializing ||= false
296
+ end
297
+
298
+ sig {returns(T::Boolean)}
299
+ def self.fully_initialized?
300
+ @fully_initialized = T.let(@fully_initialized, T.nilable(T::Boolean))
301
+ @fully_initialized ||= false
302
+ end
303
+
304
+ # Maintains the order in which values are defined
305
+ sig {params(instance: T.untyped).void}
306
+ def self._register_instance(instance)
307
+ @values ||= []
308
+ @values << T.cast(instance, T.attached_class)
309
+ end
310
+
311
+ # Entrypoint for allowing people to register new enum values.
312
+ # All enum values must be defined within this block.
313
+ sig {params(blk: T.proc.void).void}
314
+ def self.enums(&blk)
315
+ raise "enums cannot be defined for T::Enum" if self == T::Enum
316
+ raise "Enum #{self} was already initialized" if @fully_initialized
317
+ raise "Enum #{self} is still initializing" if @started_initializing
318
+
319
+ @started_initializing = true
320
+
321
+ @values = T.let(nil, T.nilable(T::Array[T.attached_class]))
322
+
323
+ yield
324
+
325
+ @mapping = T.let(nil, T.nilable(T::Hash[SerializedVal, T.attached_class]))
326
+ @mapping = {}
327
+
328
+ # Freeze the Enum class and bind the constant names into each of the instances.
329
+ self.constants(false).each do |const_name|
330
+ instance = self.const_get(const_name, false)
331
+ if !instance.is_a?(self)
332
+ raise "Invalid constant #{self}::#{const_name} on enum. " \
333
+ "All constants defined for an enum must be instances itself (e.g. `Foo = new`)."
334
+ end
335
+
336
+ instance._bind_name(const_name)
337
+ serialized = instance.serialize
338
+ if @mapping.include?(serialized)
339
+ raise "Enum values must have unique serializations. Value '#{serialized}' is repeated on #{self}."
340
+ end
341
+ @mapping[serialized] = instance
342
+ end
343
+ @values.freeze
344
+ @mapping.freeze
345
+
346
+ orphaned_instances = T.must(@values) - @mapping.values
347
+ if !orphaned_instances.empty?
348
+ raise "Enum values must be assigned to constants: #{orphaned_instances.map {|v| v.instance_variable_get('@serialized_val')}}"
349
+ end
350
+
351
+ @fully_initialized = true
352
+ end
353
+
354
+ sig {params(child_class: Module).void}
355
+ def self.inherited(child_class)
356
+ super
357
+
358
+ raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
359
+ end
360
+
361
+ # Marshal support
362
+ sig {params(_level: Integer).returns(String)}
363
+ def _dump(_level)
364
+ Marshal.dump(serialize)
365
+ end
366
+
367
+ sig {params(args: String).returns(T.attached_class)}
368
+ def self._load(args)
369
+ deserialize(Marshal.load(args))
370
+ end
371
+ end