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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Private::Types
5
+ # Wraps a proc for a type alias to defer its evaluation.
6
+ class TypeAlias < T::Types::Base
7
+
8
+ def initialize(callable)
9
+ @callable = callable
10
+ end
11
+
12
+ def aliased_type
13
+ @aliased_type ||= T::Utils.coerce(@callable.call)
14
+ end
15
+
16
+ # @override Base
17
+ def name
18
+ aliased_type.name
19
+ end
20
+
21
+ # @override Base
22
+ def recursively_valid?(obj)
23
+ aliased_type.recursively_valid?(obj)
24
+ end
25
+
26
+ # @override Base
27
+ def valid?(obj)
28
+ aliased_type.valid?(obj)
29
+ end
30
+ end
31
+ end
@@ -8,13 +8,14 @@ class T::Private::Types::Void < T::Types::Base
8
8
 
9
9
  # The actual return value of `.void` methods.
10
10
  #
11
- # Uses `Module.new` because in Ruby an anonymous module will inherit the name
12
- # of the constant it's assigned to. This gives it a readable name someone
11
+ # Uses `module VOID` because this gives it a readable name when someone
13
12
  # examines it in Pry or with `#inspect` like:
14
13
  #
15
14
  # T::Private::Types::Void::VOID
16
15
  #
17
- VOID = Module.new.freeze
16
+ module VOID
17
+ freeze
18
+ end
18
19
 
19
20
  # @override Base
20
21
  def name
@@ -10,7 +10,11 @@ module T::Profile
10
10
  def typecheck_duration_estimate
11
11
  total_typechecks = typecheck_samples * SAMPLE_RATE + (SAMPLE_RATE - typecheck_sample_attempts)
12
12
  typechecks_measured = typecheck_samples * SAMPLE_RATE
13
- typecheck_duration * SAMPLE_RATE * 1.0 * total_typechecks / typechecks_measured
13
+ if typechecks_measured.positive?
14
+ typecheck_duration * SAMPLE_RATE * 1.0 * total_typechecks / typechecks_measured
15
+ else
16
+ 0.0
17
+ end
14
18
  end
15
19
 
16
20
  def typecheck_count_estimate
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: true
3
3
 
4
4
  # A mixin for defining typed properties (attributes).
5
5
  # To get serialization methods (to/from JSON-style hashes), add T::Props::Serializable.
@@ -39,7 +39,7 @@ module T::Props
39
39
  # a document class.
40
40
  #
41
41
  # @param name [Symbol] The name of this property
42
- # @param cls [Class,T::Props::CustomType] (String) The type of this
42
+ # @param cls [Class,T::Types::Base] The type of this
43
43
  # property. If the type is itself a {Document} subclass, this
44
44
  # property will be recursively serialized/deserialized.
45
45
  # @param rules [Hash] Options to control this property's behavior.
@@ -90,10 +90,6 @@ module T::Props
90
90
  # none is provided.
91
91
  # @option rules [T::Boolean] :immutable If true, this prop cannot be
92
92
  # modified after an instance is created or loaded from a hash.
93
- # @option rules [Class,T::Props::CustomType] :array If set, specifies the
94
- # type of individual members of a prop with `type` `Array`. This
95
- # allows for typechecking of arrays, and also for recursive
96
- # serialization of arrays of sub-{Document}s.
97
93
  # @option rules [T::Boolean] :override It is an error to redeclare a
98
94
  # `prop` that has already been declared (including on a
99
95
  # superclass), unless `:override` is set to `true`.
@@ -131,7 +127,7 @@ module T::Props
131
127
  sig {params(name: T.any(Symbol, String), cls_or_args: T.untyped, args: T::Hash[Symbol, T.untyped]).void}
132
128
  def const(name, cls_or_args, args={})
133
129
  if (cls_or_args.is_a?(Hash) && cls_or_args.key?(:immutable)) || args.key?(:immutable)
134
- raise ArgumentError.new("Cannot pass 'immutable' argument when using 'const' keyword to define a prop")
130
+ Kernel.raise ArgumentError.new("Cannot pass 'immutable' argument when using 'const' keyword to define a prop")
135
131
  end
136
132
 
137
133
  if cls_or_args.is_a?(Hash)
@@ -3,18 +3,38 @@
3
3
 
4
4
  module T::Props::Constructor
5
5
  include T::Props::WeakConstructor
6
+ end
6
7
 
7
- def initialize(hash={})
8
- decorator = self.class.decorator
8
+ module T::Props::Constructor::DecoratorMethods
9
+ extend T::Sig
9
10
 
10
- decorator.props.each do |prop, rules|
11
- # It's important to explicitly compare against `true` here; the value can also be :existing or
12
- # :on_load (which are truthy) but we don't want to treat those as optional in this context.
13
- if T::Props::Utils.required_prop?(rules) && !decorator.has_default?(rules) && !hash.key?(prop)
14
- raise ArgumentError.new("Missing required prop `#{prop}` for class `#{self.class}`")
11
+ # Set values for all props that have no defaults. Override what `WeakConstructor`
12
+ # does in order to raise errors on nils instead of ignoring them.
13
+ #
14
+ # @return [Integer] A count of props that we successfully initialized (which
15
+ # we'll use to check for any unrecognized input.)
16
+ #
17
+ # checked(:never) - O(runtime object construction)
18
+ sig {params(instance: T::Props::Constructor, hash: T::Hash[Symbol, T.untyped]).returns(Integer).checked(:never)}
19
+ def construct_props_without_defaults(instance, hash)
20
+ # Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
21
+ # and therefore allocates for each entry.
22
+ result = 0
23
+ @props_without_defaults&.each_pair do |p, setter_proc|
24
+ begin
25
+ val = hash[p]
26
+ instance.instance_exec(val, &setter_proc)
27
+ if val || hash.key?(p)
28
+ result += 1
29
+ end
30
+ rescue TypeError, T::Props::InvalidValueError
31
+ if !hash.key?(p)
32
+ raise ArgumentError.new("Missing required prop `#{p}` for class `#{instance.class.name}`")
33
+ else
34
+ raise
35
+ end
15
36
  end
16
37
  end
17
-
18
- super
38
+ result
19
39
  end
20
40
  end
@@ -1,20 +1,35 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: strict
3
3
 
4
4
  module T::Props
5
5
  module CustomType
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
6
11
  include Kernel # for `is_a?`
7
12
 
8
- # Returns true if the given Ruby value can be assigned to a T::Props field
9
- # of this type.
10
- #
11
- # @param [Object] _value
12
- # @return T::Boolean
13
- def instance?(_value)
14
- raise NotImplementedError.new('Must override in included class')
13
+ # Alias for backwards compatibility
14
+ sig(:final) do
15
+ params(
16
+ value: BasicObject,
17
+ )
18
+ .returns(T::Boolean)
19
+ .checked(:never)
20
+ end
21
+ def instance?(value)
22
+ self.===(value)
15
23
  end
16
24
 
17
- # Alias for consistent interface with T::Types::Base
25
+ # Alias for backwards compatibility
26
+ sig(:final) do
27
+ params(
28
+ value: BasicObject,
29
+ )
30
+ .returns(T::Boolean)
31
+ .checked(:never)
32
+ end
18
33
  def valid?(value)
19
34
  instance?(value)
20
35
  end
@@ -24,30 +39,30 @@ module T::Props
24
39
  #
25
40
  # @param [Object] _instance
26
41
  # @return An instance of one of T::Configuration.scalar_types
27
- def serialize(_instance)
28
- raise NotImplementedError.new('Must override in included class')
29
- end
42
+ sig {abstract.params(instance: T.untyped).returns(T.untyped).checked(:never)}
43
+ def serialize(instance); end
30
44
 
31
45
  # Given the serialized form of your type, this returns an instance
32
46
  # of that custom type representing that value.
33
47
  #
34
- # @param _mongo_scalar One of T::Configuration.scalar_types
48
+ # @param scalar One of T::Configuration.scalar_types
35
49
  # @return Object
36
- def deserialize(_mongo_scalar)
37
- raise NotImplementedError.new('Must override in included class')
38
- end
50
+ sig {abstract.params(scalar: T.untyped).returns(T.untyped).checked(:never)}
51
+ def deserialize(scalar); end
39
52
 
53
+ sig {override.params(_base: Module).void}
40
54
  def self.included(_base)
41
55
  super
42
56
 
43
57
  raise 'Please use "extend", not "include" to attach this module'
44
58
  end
45
59
 
60
+ sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
46
61
  def self.scalar_type?(val)
47
62
  # We don't need to check for val's included modules in
48
63
  # T::Configuration.scalar_types, because T::Configuration.scalar_types
49
64
  # are all classes.
50
- klass = val.class
65
+ klass = T.let(val.class, T.nilable(Class))
51
66
  until klass.nil?
52
67
  return true if T::Configuration.scalar_types.include?(klass.to_s)
53
68
  klass = klass.superclass
@@ -59,16 +74,8 @@ module T::Props
59
74
  # implement set-like fields that store a unique-array, but forbid
60
75
  # hashes; Custom hash types should be implemented via an emebdded
61
76
  # T::Struct (or a subclass like Chalk::ODM::Document) or via T.
62
- def self.valid_serialization?(val, type=nil)
63
- if type&.name == 'Chalk::ODM::BsonTypes::BsonObject'
64
- # Special case we allow for backwards compatibility with props formerly
65
- # typed as "Object" or "Hash", which contain arbitrarily-nested BSON
66
- # data (e.g. parsed API request bodies). In general, we aren't pushing
67
- # to convert these to Chalk::ODM::BsonTypes - we'd rather delurk them -
68
- # but this lets us convert events with these types to Proto.
69
- return true
70
- end
71
-
77
+ sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
78
+ def self.valid_serialization?(val)
72
79
  case val
73
80
  when Array
74
81
  val.each do |v|
@@ -80,5 +87,22 @@ module T::Props
80
87
  scalar_type?(val)
81
88
  end
82
89
  end
90
+
91
+ sig(:final) do
92
+ params(instance: Object)
93
+ .returns(T.untyped)
94
+ .checked(:never)
95
+ end
96
+ def self.checked_serialize(instance)
97
+ val = T.cast(instance.class, T::Props::CustomType).serialize(instance)
98
+ unless valid_serialization?(val)
99
+ msg = "#{instance.class} did not serialize to a valid scalar type. It became a: #{val.class}"
100
+ if val.is_a?(Hash)
101
+ msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
102
+ end
103
+ raise TypeError.new(msg)
104
+ end
105
+ val
106
+ end
83
107
  end
84
108
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: strict
3
3
 
4
4
  # NB: This is not actually a decorator. It's just named that way for consistency
5
5
  # with DocumentDecorator and ModelDecorator (which both seem to have been written
@@ -10,46 +10,39 @@
10
10
  class T::Props::Decorator
11
11
  extend T::Sig
12
12
 
13
- Rules = T.type_alias(T::Hash[Symbol, T.untyped])
14
- DecoratedClass = T.type_alias(T.untyped) # T.class_of(T::Props), but that produces circular reference errors in some circumstances
15
- DecoratedInstance = T.type_alias(T.untyped) # Would be T::Props, but that produces circular reference errors in some circumstances
16
- PropType = T.type_alias(T.any(T::Types::Base, T::Props::CustomType))
17
- PropTypeOrClass = T.type_alias(T.any(PropType, Module))
13
+ Rules = T.type_alias {T::Hash[Symbol, T.untyped]}
14
+ DecoratedInstance = T.type_alias {Object} # Would be T::Props, but that produces circular reference errors in some circumstances
15
+ PropType = T.type_alias {T::Types::Base}
16
+ PropTypeOrClass = T.type_alias {T.any(PropType, Module)}
18
17
 
19
18
  class NoRulesError < StandardError; end
20
19
 
21
- T::Sig::WithoutRuntime.sig {params(klass: DecoratedClass).void}
20
+ EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
21
+ private_constant :EMPTY_PROPS
22
+
23
+ sig {params(klass: T.untyped).void.checked(:never)}
22
24
  def initialize(klass)
23
- @class = klass
24
- klass.plugins.each do |mod|
25
- Private.apply_decorator_methods(mod, self)
25
+ @class = T.let(klass, T.all(Module, T::Props::ClassMethods))
26
+ @class.plugins.each do |mod|
27
+ T::Props::Plugin::Private.apply_decorator_methods(mod, self)
26
28
  end
29
+ @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules])
27
30
  end
28
31
 
29
- # prop stuff
32
+ # checked(:never) - O(prop accesses)
30
33
  sig {returns(T::Hash[Symbol, Rules]).checked(:never)}
31
- def props
32
- @props ||= {}.freeze
33
- end
34
-
35
- # Try to avoid using this; post-definition mutation of prop rules is
36
- # surprising and hard to reason about.
37
- sig {params(prop: Symbol, key: Symbol, value: T.untyped).void}
38
- def mutate_prop_backdoor!(prop, key, value)
39
- @props = props.merge(
40
- prop => props.fetch(prop).merge(key => value).freeze
41
- ).freeze
42
- end
34
+ attr_reader :props
43
35
 
44
36
  sig {returns(T::Array[Symbol])}
45
37
  def all_props; props.keys; end
46
38
 
39
+ # checked(:never) - O(prop accesses)
47
40
  sig {params(prop: T.any(Symbol, String)).returns(Rules).checked(:never)}
48
41
  def prop_rules(prop); props[prop.to_sym] || raise("No such prop: #{prop.inspect}"); end
49
42
 
50
- sig {params(prop: Symbol, rules: Rules).void}
43
+ # checked(:never) - Rules hash is expensive to check
44
+ sig {params(prop: Symbol, rules: Rules).void.checked(:never)}
51
45
  def add_prop_definition(prop, rules)
52
- prop = prop.to_sym
53
46
  override = rules.delete(:override)
54
47
 
55
48
  if props.include?(prop) && !override
@@ -61,178 +54,147 @@ class T::Props::Decorator
61
54
  @props = @props.merge(prop => rules.freeze).freeze
62
55
  end
63
56
 
64
- sig {returns(T::Array[Symbol])}
65
- def valid_props
66
- %i{
67
- enum
68
- foreign
69
- foreign_hint_only
70
- ifunset
71
- immutable
72
- override
73
- redaction
74
- sensitivity
75
- without_accessors
76
- clobber_existing_method!
77
- extra
78
- optional
79
- _tnilable
80
- }
81
- end
82
-
83
- sig {returns(DecoratedClass).checked(:never)}
57
+ VALID_RULE_KEYS = T.let(%i[
58
+ enum
59
+ foreign
60
+ foreign_hint_only
61
+ ifunset
62
+ immutable
63
+ override
64
+ redaction
65
+ sensitivity
66
+ without_accessors
67
+ clobber_existing_method!
68
+ extra
69
+ setter_validate
70
+ _tnilable
71
+ ].map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
72
+ private_constant :VALID_RULE_KEYS
73
+
74
+ sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
75
+ def valid_rule_key?(key)
76
+ !!VALID_RULE_KEYS[key]
77
+ end
78
+
79
+ # checked(:never) - O(prop accesses)
80
+ sig {returns(T.all(Module, T::Props::ClassMethods)).checked(:never)}
84
81
  def decorated_class; @class; end
85
82
 
86
83
  # Accessors
87
84
 
85
+ # Use this to validate that a value will validate for a given prop. Useful for knowing whether a value can be set on a model without setting it.
86
+ #
87
+ # checked(:never) - potentially O(prop accesses) depending on usage pattern
88
+ sig {params(prop: Symbol, val: T.untyped).void.checked(:never)}
89
+ def validate_prop_value(prop, val)
90
+ # We call `setter_proc` here without binding to an instance, so it'll run
91
+ # `instance_variable_set` if validation passes, but nothing will care.
92
+ # We only care about the validation.
93
+ prop_rules(prop).fetch(:setter_proc).call(val)
94
+ end
95
+
88
96
  # For performance, don't use named params here.
89
97
  # Passing in rules here is purely a performance optimization.
98
+ # Unlike the other methods that take rules, this one calls prop_rules for
99
+ # the default, which raises if the prop doesn't exist (this maintains
100
+ # preexisting behavior).
101
+ #
102
+ # Note this path is NOT used by generated setters on instances,
103
+ # which are defined using `setter_proc` directly.
104
+ #
105
+ # checked(:never) - O(prop accesses)
90
106
  sig do
91
107
  params(
92
108
  instance: DecoratedInstance,
93
- prop: T.any(String, Symbol),
94
- rules: T.nilable(Rules)
109
+ prop: Symbol,
110
+ val: T.untyped,
111
+ rules: Rules
95
112
  )
96
- .returns(T.untyped)
113
+ .void
97
114
  .checked(:never)
98
115
  end
99
- def get(instance, prop, rules=props[prop.to_sym])
100
- # For backwards compatibility, fall back to reconstructing the accessor key
101
- # (though it would probably make more sense to raise in that case).
102
- instance.instance_variable_get(rules ? rules[:accessor_key] : '@' + prop.to_s) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
116
+ def prop_set(instance, prop, val, rules=prop_rules(prop))
117
+ instance.instance_exec(val, &rules.fetch(:setter_proc))
103
118
  end
119
+ alias_method :set, :prop_set
104
120
 
105
- # For performance, don't use named params here.
106
- # Passing in rules here is purely a performance optimization.
121
+ # Only Models have any custom get logic but we need to call this on
122
+ # non-Models since we don't know at code gen time what we have.
107
123
  sig do
108
124
  params(
109
125
  instance: DecoratedInstance,
110
126
  prop: Symbol,
111
- value: T.untyped,
112
- rules: T.nilable(Rules)
127
+ value: T.untyped
113
128
  )
114
- .void
129
+ .returns(T.untyped)
115
130
  .checked(:never)
116
131
  end
117
- def set(instance, prop, value, rules=props[prop.to_sym])
118
- # For backwards compatibility, fall back to reconstructing the accessor key
119
- # (though it would probably make more sense to raise in that case).
120
- instance.instance_variable_set(rules ? rules[:accessor_key] : '@' + prop.to_s, value) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
121
- end
122
-
123
- # Use this to validate that a value will validate for a given prop. Useful for knowing whether a value can be set on a model without setting it.
124
- sig {params(prop: Symbol, val: T.untyped).void.checked(:never)}
125
- def validate_prop_value(prop, val)
126
- # This implements a 'public api' on document so that we don't allow callers to pass in rules
127
- # Rules seem like an implementation detail so it seems good to now allow people to specify them manually.
128
- check_prop_type(prop, val)
129
- end
130
-
131
- # Passing in rules here is purely a performance optimization.
132
- sig {params(prop: Symbol, val: T.untyped, rules: Rules).void.checked(:never)}
133
- private def check_prop_type(prop, val, rules=prop_rules(prop))
134
- type_object = rules.fetch(:type_object)
135
- type = rules.fetch(:type)
136
-
137
- # TODO: ideally we'd add `&& rules[:optional] != :existing` to this check
138
- # (it makes sense to treat those props required in this context), but we'd need
139
- # to be sure that doesn't break any existing code first.
140
- if val.nil?
141
- if !T::Props::Utils.need_nil_write_check?(rules) || (rules.key?(:default) && rules[:default].nil?)
142
- return
143
- end
144
-
145
- if rules[:raise_on_nil_write]
146
- raise T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
147
- "(instance of #{val.class}) - need a #{type}")
148
- end
149
- end
150
-
151
- # T::Props::CustomType is not a real object based class so that we can not run real type check call.
152
- # T::Props::CustomType.valid?() is only a helper function call.
153
- valid =
154
- if type.is_a?(T::Props::CustomType) && T::Props::Utils.optional_prop?(rules)
155
- type.valid?(val)
156
- else
157
- type_object.valid?(val)
158
- end
159
- if !valid
160
- raise T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
161
- "(instance of #{val.class}) - need a #{type_object}")
162
- end
132
+ def prop_get_logic(instance, prop, value)
133
+ value
163
134
  end
164
135
 
165
136
  # For performance, don't use named params here.
166
137
  # Passing in rules here is purely a performance optimization.
167
- # Unlike the other methods that take rules, this one calls prop_rules for
168
- # the default, which raises if the prop doesn't exist (this maintains
169
- # preexisting behavior).
138
+ #
139
+ # Note this path is NOT used by generated getters on instances,
140
+ # unless `ifunset` is used on the prop, or `prop_get` is overridden.
141
+ #
142
+ # checked(:never) - O(prop accesses)
170
143
  sig do
171
144
  params(
172
145
  instance: DecoratedInstance,
173
- prop: Symbol,
174
- val: T.untyped,
175
- rules: T.nilable(Rules)
146
+ prop: T.any(String, Symbol),
147
+ rules: Rules
176
148
  )
177
- .void
149
+ .returns(T.untyped)
178
150
  .checked(:never)
179
151
  end
180
- def prop_set(instance, prop, val, rules=prop_rules(prop))
181
- check_prop_type(prop, val, T.must(rules))
182
- set(instance, prop, val, rules)
152
+ def prop_get(instance, prop, rules=prop_rules(prop))
153
+ val = instance.instance_variable_get(rules[:accessor_key])
154
+ if !val.nil?
155
+ val
156
+ elsif (d = rules[:ifunset])
157
+ T::Props::Utils.deep_clone_object(d)
158
+ else
159
+ nil
160
+ end
183
161
  end
184
162
 
185
- # For performance, don't use named params here.
186
- # Passing in rules here is purely a performance optimization.
187
163
  sig do
188
164
  params(
189
165
  instance: DecoratedInstance,
190
166
  prop: T.any(String, Symbol),
191
- rules: T.nilable(Rules)
167
+ rules: Rules
192
168
  )
193
169
  .returns(T.untyped)
194
170
  .checked(:never)
195
171
  end
196
- def prop_get(instance, prop, rules=props[prop.to_sym])
197
- val = get(instance, prop, rules)
198
-
199
- # NB: Do NOT change this to check `val.nil?` instead. BSON::ByteBuffer overrides `==` such
200
- # that `== nil` can return true while `.nil?` returns false. Tests will break in mysterious
201
- # ways. A special thanks to Ruby for enabling this type of bug.
202
- #
203
- # One side effect here is that _if_ a class (like BSON::ByteBuffer) defines ==
204
- # in such a way that instances which are not `nil`, ie are not NilClass, nevertheless
205
- # are `== nil`, then we will transparently convert such instances to `nil` on read.
206
- # Yes, our code relies on this behavior (as of writing). :thisisfine:
207
- if val != nil # rubocop:disable Style/NonNilCheck
208
- val
209
- else
210
- raise NoRulesError.new if !rules
211
- d = rules[:ifunset]
212
- if d
213
- T::Props::Utils.deep_clone_object(d)
214
- else
215
- nil
216
- end
217
- end
172
+ def prop_get_if_set(instance, prop, rules=prop_rules(prop))
173
+ instance.instance_variable_get(rules[:accessor_key])
218
174
  end
175
+ alias_method :get, :prop_get_if_set # Alias for backwards compatibility
219
176
 
177
+ # checked(:never) - O(prop accesses)
220
178
  sig do
221
179
  params(
222
180
  instance: DecoratedInstance,
223
181
  prop: Symbol,
224
182
  foreign_class: Module,
225
183
  rules: Rules,
226
- opts: Hash
184
+ opts: T::Hash[Symbol, T.untyped],
227
185
  )
228
186
  .returns(T.untyped)
229
187
  .checked(:never)
230
188
  end
231
- def foreign_prop_get(instance, prop, foreign_class, rules=props[prop.to_sym], opts={})
189
+ def foreign_prop_get(instance, prop, foreign_class, rules=prop_rules(prop), opts={})
232
190
  return if !(value = prop_get(instance, prop, rules))
233
- foreign_class.load(value, {}, opts)
191
+ T.unsafe(foreign_class).load(value, {}, opts)
234
192
  end
235
193
 
194
+ # TODO: we should really be checking all the methods on `cls`, not just Object
195
+ BANNED_METHOD_NAMES = T.let(Object.instance_methods.to_set.freeze, T::Set[Symbol])
196
+
197
+ # checked(:never) - Rules hash is expensive to check
236
198
  sig do
237
199
  params(
238
200
  name: Symbol,
@@ -241,6 +203,7 @@ class T::Props::Decorator
241
203
  type: PropTypeOrClass
242
204
  )
243
205
  .void
206
+ .checked(:never)
244
207
  end
245
208
  def prop_validate_definition!(name, cls, rules, type)
246
209
  validate_prop_name(name)
@@ -250,26 +213,17 @@ class T::Props::Decorator
250
213
  "to 'sensitivity:' (in prop #{@class.name}.#{name})")
251
214
  end
252
215
 
253
- if !(rules.keys - valid_props).empty?
216
+ if rules.keys.any? {|k| !valid_rule_key?(k)}
254
217
  raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
255
218
  end
256
219
 
257
- if (array = rules[:array])
258
- unless array.is_a?(Module)
259
- raise ArgumentError.new("Bad class as subtype in prop #{@class.name}.#{name}: #{array.inspect}")
260
- end
261
- end
262
-
263
- if !(rules[:clobber_existing_method!]) && !(rules[:without_accessors])
264
- # TODO: we should really be checking all the methods on `cls`, not just Object
265
- if Object.instance_methods.include?(name.to_sym)
266
- raise ArgumentError.new(
267
- "#{name} can't be used as a prop in #{@class} because a method with " \
268
- "that name already exists (defined by #{@class.instance_method(name).owner} " \
269
- "at #{@class.instance_method(name).source_location || '<unknown>'}). " \
270
- "(If using this name is unavoidable, try `without_accessors: true`.)"
271
- )
272
- end
220
+ if !rules[:clobber_existing_method!] && !rules[:without_accessors] && BANNED_METHOD_NAMES.include?(name.to_sym)
221
+ raise ArgumentError.new(
222
+ "#{name} can't be used as a prop in #{@class} because a method with " \
223
+ "that name already exists (defined by #{@class.instance_method(name).owner} " \
224
+ "at #{@class.instance_method(name).source_location || '<unknown>'}). " \
225
+ "(If using this name is unavoidable, try `without_accessors: true`.)"
226
+ )
273
227
  end
274
228
 
275
229
  extra = rules[:extra]
@@ -280,19 +234,16 @@ class T::Props::Decorator
280
234
  nil
281
235
  end
282
236
 
237
+ SAFE_NAME = T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp)
238
+
239
+ # Used to validate both prop names and serialized forms
240
+ sig {params(name: T.any(Symbol, String)).void}
283
241
  private def validate_prop_name(name)
284
- if name !~ /\A[A-Za-z_][A-Za-z0-9_-]*\z/
242
+ if !name.match?(SAFE_NAME)
285
243
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
286
244
  end
287
245
  end
288
246
 
289
- # Check if this cls represents a T.nilable(type)
290
- sig {params(type: PropTypeOrClass).returns(T::Boolean)}
291
- private def is_nilable?(type)
292
- return false if !type.is_a?(T::Types::Union)
293
- type.types.any? {|t| t == T::Utils.coerce(NilClass)}
294
- end
295
-
296
247
  # This converts the type from a T::Type to a regular old ruby class.
297
248
  sig {params(type: T::Types::Base).returns(Module)}
298
249
  private def convert_type_to_class(type)
@@ -321,6 +272,7 @@ class T::Props::Decorator
321
272
  end
322
273
  end
323
274
 
275
+ # checked(:never) - Rules hash is expensive to check
324
276
  sig do
325
277
  params(
326
278
  name: T.any(Symbol, String),
@@ -328,49 +280,10 @@ class T::Props::Decorator
328
280
  rules: Rules,
329
281
  )
330
282
  .void
283
+ .checked(:never)
331
284
  end
332
285
  def prop_defined(name, cls, rules={})
333
- if rules[:optional] == true
334
- T::Configuration.hard_assert_handler(
335
- 'Use of `optional: true` is deprecated, please use `T.nilable(...)` instead.',
336
- storytime: {
337
- name: name,
338
- cls_or_args: cls.to_s,
339
- args: rules,
340
- klass: decorated_class.name,
341
- },
342
- )
343
- elsif rules[:optional] == false
344
- T::Configuration.hard_assert_handler(
345
- 'Use of `optional: :false` is deprecated as it\'s the default value.',
346
- storytime: {
347
- name: name,
348
- cls_or_args: cls.to_s,
349
- args: rules,
350
- klass: decorated_class.name,
351
- },
352
- )
353
- elsif rules[:optional] == :on_load
354
- T::Configuration.hard_assert_handler(
355
- 'Use of `optional: :on_load` is deprecated. You probably want `T.nilable(...)` with :raise_on_nil_write instead.',
356
- storytime: {
357
- name: name,
358
- cls_or_args: cls.to_s,
359
- args: rules,
360
- klass: decorated_class.name,
361
- },
362
- )
363
- elsif rules[:optional] == :existing
364
- T::Configuration.hard_assert_handler(
365
- 'Use of `optional: :existing` is not allowed: you should use use T.nilable (http://go/optional)',
366
- storytime: {
367
- name: name,
368
- cls_or_args: cls.to_s,
369
- args: rules,
370
- klass: decorated_class.name,
371
- },
372
- )
373
- end
286
+ cls = T::Utils.resolve_alias(cls)
374
287
 
375
288
  if T::Utils::Nilable.is_union_with_nilclass(cls)
376
289
  # :_tnilable is introduced internally for performance purpose so that clients do not need to call
@@ -385,59 +298,34 @@ class T::Props::Decorator
385
298
  if !cls.is_a?(Module)
386
299
  cls = convert_type_to_class(cls)
387
300
  end
388
- type_object = type
389
- if !(type_object.singleton_class < T::Props::CustomType)
390
- type_object = smart_coerce(type_object, array: rules[:array], enum: rules[:enum])
391
- end
301
+ type_object = smart_coerce(type, enum: rules[:enum])
392
302
 
393
303
  prop_validate_definition!(name, cls, rules, type_object)
394
304
 
395
305
  # Retrive the possible underlying object with T.nilable.
396
- underlying_type_object = T::Utils::Nilable.get_underlying_type_object(type_object)
397
306
  type = T::Utils::Nilable.get_underlying_type(type)
398
307
 
399
- array_subdoc_type = array_subdoc_type(underlying_type_object)
400
- hash_value_subdoc_type = hash_value_subdoc_type(underlying_type_object)
401
- hash_key_custom_type = hash_key_custom_type(underlying_type_object)
402
-
403
308
  sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
404
309
  if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::Utils)
405
310
  sensitivity_and_pii = Opus::Sensitivity::Utils.normalize_sensitivity_and_pii_annotation(sensitivity_and_pii)
406
- end
407
- # We check for Class so this is only applied on concrete
408
- # documents/models; We allow mixins containing props to not
409
- # specify their PII nature, as long as every class into which they
410
- # are ultimately included does.
411
- #
412
- if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::PIIable)
413
- if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !@class.contains_pii?
311
+
312
+ # We check for Class so this is only applied on concrete
313
+ # documents/models; We allow mixins containing props to not
314
+ # specify their PII nature, as long as every class into which they
315
+ # are ultimately included does.
316
+ #
317
+ if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii?
414
318
  raise ArgumentError.new(
415
319
  'Cannot include a pii prop in a class that declares `contains_no_pii`'
416
320
  )
417
321
  end
418
322
  end
419
323
 
420
- needs_clone =
421
- if cls <= Array || cls <= Hash || cls <= Set
422
- shallow_clone_ok(underlying_type_object) ? :shallow : true
423
- else
424
- false
425
- end
426
-
427
324
  rules = rules.merge(
428
325
  # TODO: The type of this element is confusing. We should refactor so that
429
326
  # it can be always `type_object` (a PropType) or always `cls` (a Module)
430
327
  type: type,
431
- # These are precomputed for performance
432
- # TODO: A lot of these are only needed by T::Props::Serializable or T::Struct
433
- # and can/should be moved accordingly.
434
- type_is_custom_type: cls.singleton_class < T::Props::CustomType,
435
- type_is_serializable: cls < T::Props::Serializable,
436
- type_is_array_of_serializable: !array_subdoc_type.nil?,
437
- type_is_hash_of_serializable_values: !hash_value_subdoc_type.nil?,
438
- type_is_hash_of_custom_type_keys: !hash_key_custom_type.nil?,
439
328
  type_object: type_object,
440
- type_needs_clone: needs_clone,
441
329
  accessor_key: "@#{name}".to_sym,
442
330
  sensitivity: sensitivity_and_pii[:sensitivity],
443
331
  pii: sensitivity_and_pii[:pii],
@@ -447,29 +335,19 @@ class T::Props::Decorator
447
335
 
448
336
  validate_not_missing_sensitivity(name, rules)
449
337
 
450
- # for backcompat
451
- if type.is_a?(T::Types::TypedArray) && type.type.is_a?(T::Types::Simple)
452
- rules[:array] = type.type.raw_type
453
- elsif array_subdoc_type
454
- rules[:array] = array_subdoc_type
338
+ # for backcompat (the `:array` key is deprecated but because the name is
339
+ # so generic it's really hard to be sure it's not being relied on anymore)
340
+ if type.is_a?(T::Types::TypedArray)
341
+ inner = T::Utils::Nilable.get_underlying_type(type.type)
342
+ if inner.is_a?(Module)
343
+ rules[:array] = inner
344
+ end
455
345
  end
456
346
 
457
- if rules[:type_is_serializable]
458
- rules[:serializable_subtype] = cls
459
- elsif array_subdoc_type
460
- rules[:serializable_subtype] = array_subdoc_type
461
- elsif hash_value_subdoc_type && hash_key_custom_type
462
- rules[:serializable_subtype] = {
463
- keys: hash_key_custom_type,
464
- values: hash_value_subdoc_type,
465
- }
466
- elsif hash_value_subdoc_type
467
- rules[:serializable_subtype] = hash_value_subdoc_type
468
- elsif hash_key_custom_type
469
- rules[:serializable_subtype] = hash_key_custom_type
470
- end
347
+ rules[:setter_proc] = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules).freeze
471
348
 
472
349
  add_prop_definition(name, rules)
350
+
473
351
  # NB: using `without_accessors` doesn't make much sense unless you also define some other way to
474
352
  # get at the property (e.g., Chalk::ODM::Document exposes `get` and `set`).
475
353
  define_getter_and_setter(name, rules) unless rules[:without_accessors]
@@ -479,130 +357,57 @@ class T::Props::Decorator
479
357
  end
480
358
 
481
359
  handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
482
- handle_foreign_hint_only_option(cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
360
+ handle_foreign_hint_only_option(name, cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
483
361
  handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
484
362
  end
485
363
 
486
- sig {params(name: Symbol, rules: Rules).void}
364
+ # checked(:never) - Rules hash is expensive to check
365
+ sig {params(name: Symbol, rules: Rules).void.checked(:never)}
487
366
  private def define_getter_and_setter(name, rules)
488
367
  T::Configuration.without_ruby_warnings do
489
- if rules[:immutable]
490
- @class.send(:define_method, "#{name}=") do |_x|
491
- raise T::Props::ImmutableProp.new("#{self.class}##{name} cannot be modified after creation.")
492
- end
493
- else
494
- @class.send(:define_method, "#{name}=") do |x|
495
- self.class.decorator.prop_set(self, name, x, rules)
368
+ if !rules[:immutable]
369
+ if method(:prop_set).owner != T::Props::Decorator
370
+ @class.send(:define_method, "#{name}=") do |val|
371
+ T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
372
+ end
373
+ else
374
+ # Fast path (~4x faster as of Ruby 2.6)
375
+ @class.send(:define_method, "#{name}=", &rules.fetch(:setter_proc))
496
376
  end
497
377
  end
498
378
 
499
- @class.send(:define_method, name) do
500
- self.class.decorator.prop_get(self, name, rules)
501
- end
502
- end
503
- end
504
-
505
- # returns the subdoc of the array type, or nil if it's not a Document type
506
- sig do
507
- params(type: PropType)
508
- .returns(T.nilable(Module))
509
- end
510
- private def array_subdoc_type(type)
511
- if type.is_a?(T::Types::TypedArray)
512
- el_type = T::Utils.unwrap_nilable(type.type) || type.type
513
-
514
- if el_type.is_a?(T::Types::Simple) &&
515
- (el_type.raw_type < T::Props::Serializable || el_type.raw_type.is_a?(T::Props::CustomType))
516
- return el_type.raw_type
517
- end
518
- end
519
-
520
- nil
521
- end
522
-
523
- # returns the subdoc of the hash value type, or nil if it's not a Document type
524
- sig do
525
- params(type: PropType)
526
- .returns(T.nilable(Module))
527
- end
528
- private def hash_value_subdoc_type(type)
529
- if type.is_a?(T::Types::TypedHash)
530
- values_type = T::Utils.unwrap_nilable(type.values) || type.values
531
-
532
- if values_type.is_a?(T::Types::Simple) &&
533
- (values_type.raw_type < T::Props::Serializable || values_type.raw_type.is_a?(T::Props::CustomType))
534
- return values_type.raw_type
535
- end
536
- end
537
-
538
- nil
539
- end
540
-
541
- # returns the type of the hash key, or nil. Any CustomType could be a key, but we only expect Opus::Enum right now.
542
- sig do
543
- params(type: PropType)
544
- .returns(T.nilable(Module))
545
- end
546
- private def hash_key_custom_type(type)
547
- if type.is_a?(T::Types::TypedHash)
548
- keys_type = T::Utils.unwrap_nilable(type.keys) || type.keys
549
-
550
- if keys_type.is_a?(T::Types::Simple) && keys_type.raw_type.is_a?(T::Props::CustomType)
551
- return keys_type.raw_type
552
- end
553
- end
554
-
555
- nil
556
- end
557
-
558
- # From T::Props::Utils.deep_clone_object, plus String
559
- TYPES_NOT_NEEDING_CLONE = [TrueClass, FalseClass, NilClass, Symbol, String, Numeric]
560
- if defined?(Opus) && defined?(Opus::Enum)
561
- TYPES_NOT_NEEDING_CLONE << Opus::Enum
562
- end
563
-
564
- sig {params(type: PropType).returns(T::Boolean)}
565
- private def shallow_clone_ok(type)
566
- inner_type =
567
- if type.is_a?(T::Types::TypedArray)
568
- type.type
569
- elsif type.is_a?(T::Types::TypedSet)
570
- type.type
571
- elsif type.is_a?(T::Types::TypedHash)
572
- type.values
379
+ if method(:prop_get).owner != T::Props::Decorator || rules.key?(:ifunset)
380
+ @class.send(:define_method, name) do
381
+ T.unsafe(self.class).decorator.prop_get(self, name, rules)
382
+ end
383
+ else
384
+ # Fast path (~30x faster as of Ruby 2.6)
385
+ @class.send(:attr_reader, name) # send is used because `attr_reader` is private in 2.4
573
386
  end
574
-
575
- inner_type.is_a?(T::Types::Simple) && TYPES_NOT_NEEDING_CLONE.any? do |cls|
576
- inner_type.raw_type <= cls
577
387
  end
578
388
  end
579
389
 
580
390
  sig do
581
- params(type: PropTypeOrClass, array: T.untyped, enum: T.untyped)
391
+ params(type: PropTypeOrClass, enum: T.untyped)
582
392
  .returns(T::Types::Base)
583
393
  end
584
- private def smart_coerce(type, array:, enum:)
394
+ private def smart_coerce(type, enum:)
585
395
  # Backwards compatibility for pre-T::Types style
586
- if !array.nil? && !enum.nil?
587
- raise ArgumentError.new("Cannot specify both :array and :enum options")
588
- elsif !array.nil?
589
- if type == Set
590
- T::Set[array]
591
- else
592
- T::Array[array]
593
- end
594
- elsif !enum.nil?
595
- if T::Utils.unwrap_nilable(type)
596
- T.nilable(T.enum(enum))
396
+ type = T::Utils.coerce(type)
397
+ if enum.nil?
398
+ type
399
+ else
400
+ nonnil_type = T::Utils.unwrap_nilable(type)
401
+ if nonnil_type
402
+ T.nilable(T.all(nonnil_type, T.enum(enum)))
597
403
  else
598
- T.enum(enum)
404
+ T.all(type, T.enum(enum))
599
405
  end
600
- else
601
- T::Utils.coerce(type)
602
406
  end
603
407
  end
604
408
 
605
- sig {params(prop_name: Symbol, rules: Hash).void}
409
+ # checked(:never) - Rules hash is expensive to check
410
+ sig {params(prop_name: Symbol, rules: Rules).void.checked(:never)}
606
411
  private def validate_not_missing_sensitivity(prop_name, rules)
607
412
  if rules[:sensitivity].nil?
608
413
  if rules[:redaction]
@@ -617,7 +422,7 @@ class T::Props::Decorator
617
422
  # TODO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
618
423
  # other terms, but this interacts badly with ProtoDefinedDocument because
619
424
  # the proto syntax currently can't declare "sensitivity: []"
620
- if prop_name =~ /\bsecret\b/
425
+ if /\bsecret\b/.match?(prop_name)
621
426
  T::Configuration.hard_assert_handler(
622
427
  "#{@class}##{prop_name} has the word 'secret' in its name, but no " \
623
428
  "'sensitivity:' annotation. This is probably wrong, because if a " \
@@ -634,7 +439,7 @@ class T::Props::Decorator
634
439
  sig do
635
440
  params(
636
441
  prop_name: Symbol,
637
- redaction: Chalk::Tools::RedactionUtils::RedactionDirectiveSpec,
442
+ redaction: T.untyped,
638
443
  )
639
444
  .void
640
445
  end
@@ -671,13 +476,14 @@ class T::Props::Decorator
671
476
 
672
477
  sig do
673
478
  params(
479
+ prop_name: Symbol,
674
480
  prop_cls: Module,
675
481
  foreign_hint_only: T.untyped,
676
482
  )
677
483
  .void
678
484
  end
679
- private def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)
680
- if ![String, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
485
+ private def handle_foreign_hint_only_option(prop_name, prop_cls, foreign_hint_only)
486
+ if ![String, Array].include?(prop_cls) && !prop_cls.is_a?(T::Props::CustomType)
681
487
  raise ArgumentError.new(
682
488
  "`foreign_hint_only` can only be used with String or Array prop types"
683
489
  )
@@ -687,8 +493,24 @@ class T::Props::Decorator
687
493
  :foreign_hint_only, foreign_hint_only,
688
494
  valid_type_msg: "an individual or array of a model class, or a Proc returning such."
689
495
  )
496
+
497
+ unless foreign_hint_only.is_a?(Proc)
498
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign_hint_only}, notify: 'jerry')
499
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign_hint_only`. In other words:
500
+
501
+ instead of `prop :foo, String, foreign_hint_only: FooModel`
502
+ use `prop :foo, String, foreign_hint_only: -> {FooModel}`
503
+
504
+ OR
505
+
506
+ instead of `prop :foo, String, foreign_hint_only: [FooModel, BarModel]`
507
+ use `prop :foo, String, foreign_hint_only: -> {[FooModel, BarModel]}`
508
+
509
+ MESSAGE
510
+ end
690
511
  end
691
512
 
513
+ # checked(:never) - Rules hash is expensive to check
692
514
  sig do
693
515
  params(
694
516
  prop_name: T.any(String, Symbol),
@@ -696,6 +518,7 @@ class T::Props::Decorator
696
518
  foreign: T.untyped,
697
519
  )
698
520
  .void
521
+ .checked(:never)
699
522
  end
700
523
  private def define_foreign_method(prop_name, rules, foreign)
701
524
  fk_method = "#{prop_name}_"
@@ -705,6 +528,7 @@ class T::Props::Decorator
705
528
  # *haven't* allowed additional options in the past and want to
706
529
  # default to keeping this interface narrow.
707
530
  @class.send(:define_method, fk_method) do |allow_direct_mutation: nil|
531
+ foreign = T.let(foreign, T.untyped)
708
532
  if foreign.is_a?(Proc)
709
533
  resolved_foreign = foreign.call
710
534
  if !resolved_foreign.respond_to?(:load)
@@ -717,13 +541,13 @@ class T::Props::Decorator
717
541
  # of the method, optimizing it so this only runs on the first invocation.
718
542
  foreign = resolved_foreign
719
543
  end
720
- if allow_direct_mutation.nil?
721
- opts = {}
544
+ opts = if allow_direct_mutation.nil?
545
+ {}
722
546
  else
723
- opts = {allow_direct_mutation: allow_direct_mutation}
547
+ {allow_direct_mutation: allow_direct_mutation}
724
548
  end
725
549
 
726
- self.class.decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
550
+ T.unsafe(self.class).decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
727
551
  end
728
552
 
729
553
  force_fk_method = "#{fk_method}!"
@@ -735,18 +559,11 @@ class T::Props::Decorator
735
559
  storytime: {method: force_fk_method, class: self.class}
736
560
  )
737
561
  end
738
- T.must(loaded_foreign)
739
- end
740
-
741
- @class.send(:define_method, "#{prop_name}_record") do |allow_direct_mutation: nil|
742
- T::Configuration.soft_assert_handler(
743
- "Using deprecated 'model.#{prop_name}_record' foreign key syntax. You should replace this with 'model.#{prop_name}_'",
744
- notify: 'vasi'
745
- )
746
- send(fk_method, allow_direct_mutation: allow_direct_mutation)
562
+ loaded_foreign
747
563
  end
748
564
  end
749
565
 
566
+ # checked(:never) - Rules hash is expensive to check
750
567
  sig do
751
568
  params(
752
569
  prop_name: Symbol,
@@ -755,6 +572,7 @@ class T::Props::Decorator
755
572
  foreign: T.untyped,
756
573
  )
757
574
  .void
575
+ .checked(:never)
758
576
  end
759
577
  private def handle_foreign_option(prop_name, prop_cls, rules, foreign)
760
578
  validate_foreign_option(
@@ -775,6 +593,16 @@ class T::Props::Decorator
775
593
  )
776
594
  end
777
595
 
596
+ unless foreign.is_a?(Proc)
597
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign}, notify: 'jerry')
598
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign`. In other words:
599
+
600
+ instead of `prop :foo, String, foreign: FooModel`
601
+ use `prop :foo, String, foreign: -> {FooModel}`
602
+
603
+ MESSAGE
604
+ end
605
+
778
606
  define_foreign_method(prop_name, rules, foreign)
779
607
  end
780
608
 
@@ -782,16 +610,17 @@ class T::Props::Decorator
782
610
  #
783
611
  # This gets called when a module or class that extends T::Props gets included, extended,
784
612
  # prepended, or inherited.
785
- T::Sig::WithoutRuntime.sig {params(child: DecoratedClass).void}
613
+ sig {params(child: Module).void.checked(:never)}
786
614
  def model_inherited(child)
787
615
  child.extend(T::Props::ClassMethods)
788
- child.plugins.concat(decorated_class.plugins)
616
+ child = T.cast(child, T.all(Module, T::Props::ClassMethods))
789
617
 
618
+ child.plugins.concat(decorated_class.plugins)
790
619
  decorated_class.plugins.each do |mod|
791
620
  # NB: apply_class_methods must not be an instance method on the decorator itself,
792
621
  # otherwise we'd have to call child.decorator here, which would create the decorator
793
622
  # before any `decorator_class` override has a chance to take effect (see the comment below).
794
- Private.apply_class_methods(mod, child)
623
+ T::Props::Plugin::Private.apply_class_methods(mod, child)
795
624
  end
796
625
 
797
626
  props.each do |name, rules|
@@ -800,33 +629,47 @@ class T::Props::Decorator
800
629
  # time. Any class that defines props and also overrides the `decorator_class` method is going
801
630
  # to reach this line before its override take effect, turning it into a no-op.
802
631
  child.decorator.add_prop_definition(name, copied_rules)
632
+
633
+ # It's a bit tricky to support `prop_get` hooks added by plugins without
634
+ # sacrificing the `attr_reader` fast path or clobbering customized getters
635
+ # defined manually on a child.
636
+ #
637
+ # To make this work, we _do_ clobber getters defined on the child, but only if:
638
+ # (a) it's needed in order to support a `prop_get` hook, and
639
+ # (b) it's safe because the getter was defined by this file.
640
+ #
641
+ unless rules[:without_accessors]
642
+ if clobber_getter?(child, name)
643
+ child.send(:define_method, name) do
644
+ T.unsafe(self.class).decorator.prop_get(self, name, rules)
645
+ end
646
+ end
647
+
648
+ if !rules[:immutable] && clobber_setter?(child, name)
649
+ child.send(:define_method, "#{name}=") do |val|
650
+ T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
651
+ end
652
+ end
653
+ end
803
654
  end
804
655
  end
805
656
 
806
- T::Sig::WithoutRuntime.sig {params(mod: Module).void}
657
+ sig {params(child: T.all(Module, T::Props::ClassMethods), prop: Symbol).returns(T::Boolean).checked(:never)}
658
+ private def clobber_getter?(child, prop)
659
+ !!(child.decorator.method(:prop_get).owner != method(:prop_get).owner &&
660
+ child.instance_method(prop).source_location&.first == __FILE__)
661
+ end
662
+
663
+ sig {params(child: T.all(Module, T::Props::ClassMethods), prop: Symbol).returns(T::Boolean).checked(:never)}
664
+ private def clobber_setter?(child, prop)
665
+ !!(child.decorator.method(:prop_set).owner != method(:prop_set).owner &&
666
+ child.instance_method("#{prop}=").source_location&.first == __FILE__)
667
+ end
668
+
669
+ sig {params(mod: Module).void.checked(:never)}
807
670
  def plugin(mod)
808
671
  decorated_class.plugins << mod
809
- Private.apply_class_methods(mod, decorated_class)
810
- Private.apply_decorator_methods(mod, self)
811
- end
812
-
813
- module Private
814
- # These need to be non-instance methods so we can use them without prematurely creating the
815
- # child decorator in `model_inherited` (see comments there for details).
816
- def self.apply_class_methods(plugin, target)
817
- if plugin.const_defined?('ClassMethods')
818
- # FIXME: This will break preloading, selective test execution, etc if `mod::ClassMethods`
819
- # is ever defined in a separate file from `mod`.
820
- target.extend(plugin::ClassMethods) # rubocop:disable PrisonGuard/NoDynamicConstAccess
821
- end
822
- end
823
-
824
- def self.apply_decorator_methods(plugin, target)
825
- if plugin.const_defined?('DecoratorMethods')
826
- # FIXME: This will break preloading, selective test execution, etc if `mod::DecoratorMethods`
827
- # is ever defined in a separate file from `mod`.
828
- target.extend(plugin::DecoratorMethods) # rubocop:disable PrisonGuard/NoDynamicConstAccess
829
- end
830
- end
672
+ T::Props::Plugin::Private.apply_class_methods(mod, decorated_class)
673
+ T::Props::Plugin::Private.apply_decorator_methods(mod, self)
831
674
  end
832
675
  end