sorbet-runtime 0.6.13299 → 0.6.13300

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 346fdbb1a44759ff22aa86b6583474a790fb38fea59464ecee830b515521a4cf
4
- data.tar.gz: 5a3a3fb1fcd5b8801ea0070631caa24a1decb95df7c9d09e506f16910ffb819b
3
+ metadata.gz: 24e2e39ecb07eff0a9d4e9efeeebe17935b16363cf0e27b070e9ac9b5e90eccf
4
+ data.tar.gz: 7e4c6aca4672cfe3aa3a33779e640a343c981330fbda03a2d1471b7ac619ae20
5
5
  SHA512:
6
- metadata.gz: 87b79a12e41334bee22330e734b8613515da22e3d7d1843d60584e20ff41dd0fb5ab896265e7d1434cb1d09c681ed51c603b0ae4c6c976afabbf8c18ec7021e2
7
- data.tar.gz: aa62c20849e24e1453150fd7e4cf354bf36d5ee24c355a3b35a4e39d0263a2821e3c1f9cbec4521245e45b2639000441ccb85567a5558525896faae3ab62b720
6
+ metadata.gz: 86e102983a1b56070cc100e77a809cf77ffdc4cc0c3ea3d4986020e9ecf85763af195cd24517157ddf2d2d4ed25c32ba9d92a19ecb1857c61b2c9503e737ac98
7
+ data.tar.gz: 63d055b3273fff38053757d9e439a8e82f945d1edc0d883b2a82a1f442feb13af73a199eb56aa5216aa67e2f9c749431c29a50b5d27d24e483f580d4beca7fb5
@@ -20,10 +20,10 @@ module T::Props::Constructor::DecoratorMethods
20
20
  # Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
21
21
  # and therefore allocates for each entry.
22
22
  result = 0
23
- props_without_defaults&.each_pair do |p, setter_proc|
23
+ props_without_defaults&.each_pair do |p, bound_setter|
24
24
  begin
25
25
  val = hash[p]
26
- instance.instance_exec(val, &setter_proc)
26
+ bound_setter.call(instance, val)
27
27
  if val || hash.key?(p)
28
28
  result += 1
29
29
  end
@@ -62,9 +62,15 @@ module T::Props
62
62
  # We don't need to check for val's included modules in
63
63
  # T::Configuration.scalar_types, because T::Configuration.scalar_types
64
64
  # are all classes.
65
+ #
66
+ # `name` rather than `to_s`: identical for real classes, but returns the
67
+ # cached frozen string (Ruby 3.2+) where to_s allocates per call.
68
+ # Anonymous classes yield nil, and `include?(nil)` is false (an
69
+ # anonymous class can never be a registered scalar type).
70
+ scalar_types = T::Configuration.scalar_types
65
71
  klass = val.class
66
72
  until klass.nil?
67
- return true if T::Configuration.scalar_types.include?(klass.to_s)
73
+ return true if scalar_types.include?(klass.name)
68
74
  klass = klass.superclass
69
75
  end
70
76
  false
@@ -71,7 +71,12 @@ class T::Props::Decorator
71
71
  raise ArgumentError.new("Attempted to redefine prop #{name.inspect} on class #{@class} that's already defined without specifying :override => true: #{prop_rules(name)}")
72
72
  end
73
73
 
74
- @props = @props.merge(name => rules.freeze).freeze
74
+ # dup/store/freeze rather than @props.merge(name => rules.freeze).freeze:
75
+ # merge would allocate a temporary single-entry Hash and re-insert every
76
+ # existing entry. The published hash stays frozen between adds.
77
+ new_props = @props.dup
78
+ new_props[name] = rules.freeze
79
+ @props = new_props.freeze
75
80
  end
76
81
 
77
82
  # Heads up!
@@ -126,7 +131,7 @@ class T::Props::Decorator
126
131
  # preexisting behavior).
127
132
  #
128
133
  # Note this path is NOT used by generated setters on instances,
129
- # which are defined using `setter_proc` directly.
134
+ # which are defined using the 1-arity `:setter_proc` directly.
130
135
  #
131
136
  # checked(:never) - O(prop accesses)
132
137
  sig do
@@ -140,7 +145,9 @@ class T::Props::Decorator
140
145
  .checked(:never)
141
146
  end
142
147
  def prop_set(instance, prop, val, rules=prop_rules(prop))
143
- instance.instance_exec(val, &rules.fetch(:setter_proc))
148
+ # The 2-arity bound proc does the same validation/assignment as the
149
+ # 1-arity setter proc without instance_exec's per-call self-rebinding.
150
+ rules.fetch(:_bound_setter_proc).call(instance, val)
144
151
  end
145
152
  alias_method :set, :prop_set
146
153
 
@@ -182,7 +189,7 @@ class T::Props::Decorator
182
189
  if !val.nil?
183
190
  val
184
191
  elsif (d = rules[:ifunset])
185
- T::Props::Utils.deep_clone_object(d)
192
+ T::Props::Utils.deep_clone(d)
186
193
  else
187
194
  nil
188
195
  end
@@ -421,11 +428,20 @@ class T::Props::Decorator
421
428
  end
422
429
  end
423
430
 
424
- setter_proc, value_validate_proc = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules)
431
+ setter_proc, value_validate_proc, bound_setter_proc = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules)
425
432
  setter_proc.freeze
426
433
  value_validate_proc.freeze
427
- rules[:setter_proc] = setter_proc
434
+ bound_setter_proc.freeze
428
435
  rules[:value_validate_proc] = value_validate_proc
436
+ # :setter_proc is a pre-existing rules key and is preserved as-is: the
437
+ # 1-arity, self-bound proc used only to define the generated `name=`
438
+ # instance setter via `define_method(&proc)` (its fast path). The net-new
439
+ # :_bound_setter_proc is the 2-arity (instance, val) equivalent that every
440
+ # other write goes through without instance_exec's per-call self-rebinding;
441
+ # it is underscore-prefixed (like :_tnilable) so external code that replays
442
+ # rules hashes back into prop() filters it out as internal.
443
+ rules[:setter_proc] = setter_proc
444
+ rules[:_bound_setter_proc] = bound_setter_proc
429
445
 
430
446
  validate_overrides(name, rules)
431
447
  add_prop_definition(name, rules)
@@ -644,12 +660,25 @@ class T::Props::Decorator
644
660
  T::Props::Plugin::Private.apply_class_methods(mod, child)
645
661
  end
646
662
 
647
- props.each do |name, rules|
663
+ parent_props = props
664
+ # Return before child.decorator below: forcing decorator creation for a
665
+ # prop-less parent would defeat any decorator_class override (see the NB).
666
+ return if parent_props.empty?
667
+
668
+ # NB: Calling `child.decorator` here is a time bomb that's going to give someone a really bad
669
+ # time. Any class that defines props and also overrides the `decorator_class` method is going
670
+ # to reach this line before its override take effect, turning it into a no-op.
671
+ child_decorator = child.decorator
672
+
673
+ # Computed once for all props: the owner comparison cannot change inside
674
+ # the loop below (it only defines prop accessors on child), and each
675
+ # Object#method call allocates a fresh Method.
676
+ clobber_getters = child_decorator.method(:prop_get).owner != method(:prop_get).owner
677
+ clobber_setters = child_decorator.method(:prop_set).owner != method(:prop_set).owner
678
+
679
+ parent_props.each do |name, rules|
648
680
  copied_rules = rules.dup
649
- # NB: Calling `child.decorator` here is a time bomb that's going to give someone a really bad
650
- # time. Any class that defines props and also overrides the `decorator_class` method is going
651
- # to reach this line before its override take effect, turning it into a no-op.
652
- child.decorator.add_prop_definition(name, copied_rules)
681
+ child_decorator.add_prop_definition(name, copied_rules)
653
682
 
654
683
  # It's a bit tricky to support `prop_get` hooks added by plugins without
655
684
  # sacrificing the `attr_reader` fast path or clobbering customized getters
@@ -660,13 +689,13 @@ class T::Props::Decorator
660
689
  # (b) it's safe because the getter was defined by this file.
661
690
  #
662
691
  unless rules[:without_accessors]
663
- if clobber_getter?(child, name)
692
+ if clobber_getters && child.instance_method(name).source_location&.first == __FILE__
664
693
  child.send(:define_method, name) do
665
694
  T.unsafe(self.class).decorator.prop_get(self, name, rules)
666
695
  end
667
696
  end
668
697
 
669
- if !rules[:immutable] && clobber_setter?(child, name)
698
+ if !rules[:immutable] && clobber_setters && child.instance_method("#{name}=").source_location&.first == __FILE__
670
699
  child.send(:define_method, "#{name}=") do |val|
671
700
  T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
672
701
  end
@@ -721,18 +750,6 @@ class T::Props::Decorator
721
750
  result
722
751
  end
723
752
 
724
- sig { params(child: DecoratedClassType, prop: Symbol).returns(T::Boolean).checked(:never) }
725
- private def clobber_getter?(child, prop)
726
- !!(child.decorator.method(:prop_get).owner != method(:prop_get).owner &&
727
- child.instance_method(prop).source_location&.first == __FILE__)
728
- end
729
-
730
- sig { params(child: DecoratedClassType, prop: Symbol).returns(T::Boolean).checked(:never) }
731
- private def clobber_setter?(child, prop)
732
- !!(child.decorator.method(:prop_set).owner != method(:prop_set).owner &&
733
- child.instance_method("#{prop}=").source_location&.first == __FILE__)
734
- end
735
-
736
753
  sig { params(mod: T::Module[T.anything]).void.checked(:never) }
737
754
  def plugin(mod)
738
755
  decorated_class.plugins << mod
@@ -119,7 +119,7 @@ module T::Props
119
119
 
120
120
  private_class_method def self.validate_deserialize_ivar_set(clause)
121
121
  # %<accessor_key>s = if val.nil?
122
- # found -= 1 unless hash.key?(%<serialized_form>s)
122
+ # found -= 1 unless hash.key?(%<serialized_form>s.freeze)
123
123
  # %<nil_handler>s
124
124
  # else
125
125
  # %<serialized_val>s
@@ -143,7 +143,13 @@ module T::Props
143
143
  receiver, method, arg = found_condition.children
144
144
  assert_equal(s(:lvar, :hash), receiver)
145
145
  assert_equal(:key?, method)
146
- assert_equal(:str, arg.type)
146
+ # hash.key?(%<serialized_form>s.freeze), where `.freeze` on a string
147
+ # literal is side-effect-free (and avoids a per-call allocation)
148
+ assert_equal(:send, arg.type)
149
+ arg_receiver, arg_method, *arg_rest = arg.children
150
+ assert_equal(:str, arg_receiver.type)
151
+ assert_equal(:freeze, arg_method)
152
+ assert_equal([], arg_rest)
147
153
  assert_equal(nil, found_if_body)
148
154
  assert_equal(s(:op_asgn, s(:lvasgn, :found), :-, s(:int, 1)), found_else_body)
149
155
 
@@ -262,7 +268,7 @@ module T::Props
262
268
  @whitelisted_methods_for_serialize ||= {
263
269
  lvar: %i{dup map transform_values transform_keys each_with_object nil? []= serialize},
264
270
  ivar: %i[dup map transform_values transform_keys each_with_object serialize],
265
- const: %i[checked_serialize deep_clone_object],
271
+ const: %i[checked_serialize deep_clone deep_clone_object],
266
272
  }
267
273
  end
268
274
 
@@ -270,7 +276,7 @@ module T::Props
270
276
  private_class_method def self.whitelisted_methods_for_deserialize
271
277
  @whitelisted_methods_for_deserialize ||= {
272
278
  lvar: %i{dup map transform_values transform_keys each_with_object nil? []= to_f},
273
- const: %i[deserialize from_hash deep_clone_object],
279
+ const: %i[deserialize from_hash deep_clone deep_clone_object],
274
280
  }
275
281
  end
276
282
  end
@@ -59,13 +59,23 @@ module T::Props
59
59
  raise SourceEvaluationDisabled.new
60
60
  end
61
61
 
62
- source = lazily_defined_methods.fetch(name).call
62
+ blk = lazily_defined_methods[name]
63
+ # A concurrent first call can have already evaluated and removed
64
+ # this entry; the specialized method is installed, so the
65
+ # placeholder's retry dispatch will reach it directly.
66
+ return if blk.nil?
67
+
68
+ source = blk.call
63
69
 
64
70
  cls = decorated_class
65
71
  T::Configuration.without_ruby_warnings do
66
72
  cls.class_eval(source.to_s)
67
73
  end
68
74
  cls.send(:private, name)
75
+ # Removing the entry records that no placeholder is installed, so a
76
+ # later prop addition (possible, if unusual: props added after first
77
+ # use) re-enqueues a fresh placeholder instead of being skipped.
78
+ lazily_defined_methods.delete(name)
69
79
  end
70
80
 
71
81
  sig { params(name: Symbol).void }
@@ -74,15 +84,31 @@ module T::Props
74
84
  raise SourceEvaluationDisabled.new
75
85
  end
76
86
 
77
- lazily_defined_vm_methods.fetch(name).call
87
+ blk = lazily_defined_vm_methods[name]
88
+ # See eval_lazily_defined_method!.
89
+ return if blk.nil?
90
+
91
+ blk.call
78
92
 
79
93
  cls = decorated_class
80
94
  cls.send(:private, name)
95
+ lazily_defined_vm_methods.delete(name)
81
96
  end
82
97
 
83
98
  sig { params(name: Symbol, blk: T.proc.returns(String)).void }
84
99
  private def enqueue_lazy_method_definition!(name, &blk)
85
- lazily_defined_methods[name] = blk
100
+ methods = lazily_defined_methods
101
+ if methods.key?(name)
102
+ # The placeholder installed below is already in place (every prop
103
+ # addition lands here, so this is hit from the second prop of a
104
+ # class onward, and again for every prop re-added to a subclass).
105
+ # It reads the generator from the hash at call time, so updating
106
+ # the entry suffices; skipping the re-install avoids 2-4 method
107
+ # table writes (and their cache invalidations) per addition.
108
+ methods[name] = blk
109
+ return
110
+ end
111
+ methods[name] = blk
86
112
 
87
113
  cls = decorated_class
88
114
  if cls.method_defined?(name) || cls.private_method_defined?(name)
@@ -102,7 +128,13 @@ module T::Props
102
128
 
103
129
  sig { params(name: Symbol, blk: T.untyped).void }
104
130
  private def enqueue_lazy_vm_method_definition!(name, &blk)
105
- lazily_defined_vm_methods[name] = blk
131
+ methods = lazily_defined_vm_methods
132
+ if methods.key?(name)
133
+ # See enqueue_lazy_method_definition!.
134
+ methods[name] = blk
135
+ return
136
+ end
137
+ methods[name] = blk
106
138
 
107
139
  cls = decorated_class
108
140
  cls.send(:define_method, name) do |*args|
@@ -47,7 +47,7 @@ module T::Props::Optional::DecoratorMethods
47
47
  attr_reader :props_with_defaults
48
48
 
49
49
  # checked(:never) - O(runtime object construction)
50
- sig { returns(T::Hash[Symbol, T::Props::Private::SetterFactory::SetterProc]).checked(:never) }
50
+ sig { returns(T::Hash[Symbol, T::Props::Private::SetterFactory::BoundSetterProc]).checked(:never) }
51
51
  attr_reader :props_without_defaults
52
52
 
53
53
  def add_prop_definition(prop, rules)
@@ -62,7 +62,7 @@ module T::Props::Optional::DecoratorMethods
62
62
  rules[DEFAULT_SETTER_RULE_KEY] = default_setter
63
63
  else
64
64
  @props_without_defaults ||= {}
65
- @props_without_defaults[prop] = rules.fetch(:setter_proc)
65
+ @props_without_defaults[prop] = rules.fetch(:_bound_setter_proc)
66
66
  props_with_defaults&.delete(prop) # Handle potential override
67
67
  end
68
68
 
@@ -9,14 +9,14 @@ module T::Props
9
9
  abstract!
10
10
 
11
11
  # checked(:never) - O(object construction x prop count)
12
- sig { returns(SetterFactory::SetterProc).checked(:never) }
13
- attr_reader :setter_proc
12
+ sig { returns(SetterFactory::BoundSetterProc).checked(:never) }
13
+ attr_reader :bound_setter_proc
14
14
 
15
15
  # checked(:never) - We do this with `T.let` instead
16
- sig { params(accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never) }
17
- def initialize(accessor_key, setter_proc)
16
+ sig { params(accessor_key: Symbol, bound_setter_proc: SetterFactory::BoundSetterProc).void.checked(:never) }
17
+ def initialize(accessor_key, bound_setter_proc)
18
18
  @accessor_key = T.let(accessor_key, Symbol)
19
- @setter_proc = T.let(setter_proc, SetterFactory::SetterProc)
19
+ @bound_setter_proc = T.let(bound_setter_proc, SetterFactory::BoundSetterProc)
20
20
  end
21
21
 
22
22
  # checked(:never) - O(object construction x prop count)
@@ -33,30 +33,30 @@ module T::Props
33
33
  sig { params(cls: T::Module[T.anything], rules: T::Hash[Symbol, T.untyped]).returns(T.nilable(ApplyDefault)).checked(:never) }
34
34
  def self.for(cls, rules)
35
35
  accessor_key = rules.fetch(:accessor_key)
36
- setter = rules.fetch(:setter_proc)
36
+ bound_setter = rules.fetch(:_bound_setter_proc)
37
37
 
38
38
  if rules.key?(:factory)
39
- ApplyDefaultFactory.new(cls, rules.fetch(:factory), accessor_key, setter)
39
+ ApplyDefaultFactory.new(cls, rules.fetch(:factory), accessor_key, bound_setter)
40
40
  elsif rules.key?(:default)
41
41
  default = rules.fetch(:default)
42
42
  case default
43
43
  when *NO_CLONE_TYPES
44
- return ApplyPrimitiveDefault.new(default, accessor_key, setter)
44
+ return ApplyPrimitiveDefault.new(default, accessor_key, bound_setter)
45
45
  when String
46
46
  if default.frozen?
47
- return ApplyPrimitiveDefault.new(default, accessor_key, setter)
47
+ return ApplyPrimitiveDefault.new(default, accessor_key, bound_setter)
48
48
  end
49
49
  when Array
50
50
  if default.empty? && default.class == Array
51
- return ApplyEmptyArrayDefault.new(accessor_key, setter)
51
+ return ApplyEmptyArrayDefault.new(accessor_key, bound_setter)
52
52
  end
53
53
  when Hash
54
54
  if default.empty? && default.default.nil? && T.unsafe(default).default_proc.nil? && default.class == Hash
55
- return ApplyEmptyHashDefault.new(accessor_key, setter)
55
+ return ApplyEmptyHashDefault.new(accessor_key, bound_setter)
56
56
  end
57
57
  end
58
58
 
59
- ApplyComplexDefault.new(default, accessor_key, setter)
59
+ ApplyComplexDefault.new(default, accessor_key, bound_setter)
60
60
  else
61
61
  nil
62
62
  end
@@ -67,16 +67,16 @@ module T::Props
67
67
  abstract!
68
68
 
69
69
  # checked(:never) - We do this with `T.let` instead
70
- sig { params(default: BasicObject, accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never) }
71
- def initialize(default, accessor_key, setter_proc)
70
+ sig { params(default: BasicObject, accessor_key: Symbol, bound_setter_proc: SetterFactory::BoundSetterProc).void.checked(:never) }
71
+ def initialize(default, accessor_key, bound_setter_proc)
72
72
  # FIXME: Ideally we'd check here that the default is actually a valid
73
73
  # value for this field, but existing code relies on the fact that we don't.
74
74
  #
75
75
  # :(
76
76
  #
77
- # setter_proc.call(default)
77
+ # bound_setter_proc.call(instance, default)
78
78
  @default = T.let(default, BasicObject)
79
- super(accessor_key, setter_proc)
79
+ super(accessor_key, bound_setter_proc)
80
80
  end
81
81
 
82
82
  # checked(:never) - O(object construction x prop count)
@@ -96,7 +96,7 @@ module T::Props
96
96
  # checked(:never) - O(object construction x prop count)
97
97
  sig { override.returns(T.untyped).checked(:never) }
98
98
  def default
99
- T::Props::Utils.deep_clone_object(@default)
99
+ T::Props::Utils.deep_clone(@default)
100
100
  end
101
101
  end
102
102
 
@@ -141,15 +141,15 @@ module T::Props
141
141
  cls: T::Module[T.anything],
142
142
  factory: T.any(Proc, Method),
143
143
  accessor_key: Symbol,
144
- setter_proc: SetterFactory::SetterProc,
144
+ bound_setter_proc: SetterFactory::BoundSetterProc,
145
145
  )
146
146
  .void
147
147
  .checked(:never)
148
148
  end
149
- def initialize(cls, factory, accessor_key, setter_proc)
149
+ def initialize(cls, factory, accessor_key, bound_setter_proc)
150
150
  @class = T.let(cls, T::Module[T.anything])
151
151
  @factory = T.let(factory, T.any(Proc, Method))
152
- super(accessor_key, setter_proc)
152
+ super(accessor_key, bound_setter_proc)
153
153
  end
154
154
 
155
155
  # checked(:never) - O(object construction x prop count)
@@ -157,7 +157,7 @@ module T::Props
157
157
  def set_default(instance)
158
158
  # Use the actual setter to validate the factory returns a legitimate
159
159
  # value every time
160
- instance.instance_exec(default, &@setter_proc)
160
+ @bound_setter_proc.call(instance, default)
161
161
  end
162
162
 
163
163
  # checked(:never) - O(object construction x prop count)
@@ -81,10 +81,16 @@ module T::Props
81
81
  raise_on_nil_write: !!rules[:raise_on_nil_write],
82
82
  )
83
83
 
84
+ # The `.freeze` on the `hash.key?` argument matters: when this source
85
+ # is eval'd lazily (without a frozen-string-literal prefix, unlike
86
+ # `eagerly_define_lazy_methods!`), a bare literal here would allocate
87
+ # a new String on every call for each nil/missing prop. `"str".freeze`
88
+ # compiles to a no-allocation VM instruction (opt_str_freeze). The
89
+ # `hash[...]` read doesn't need it because of opt_aref_with.
84
90
  <<~RUBY
85
91
  val = hash[#{hash_key.inspect}]
86
92
  #{ivar_name} = if val.nil?
87
- found -= 1 unless hash.key?(#{hash_key.inspect})
93
+ found -= 1 unless hash.key?(#{hash_key.inspect}.freeze)
88
94
  #{nil_handler}
89
95
  else
90
96
  #{transformed_val}
@@ -88,7 +88,7 @@ module T::Props
88
88
  # string comparisons.
89
89
  nil
90
90
  else
91
- "T::Props::Utils.deep_clone_object(#{varname})"
91
+ "T::Props::Utils.deep_clone(#{varname})"
92
92
  end
93
93
  when T::Types::Union
94
94
  non_nil_type = T::Utils.unwrap_nilable(type)
@@ -110,10 +110,10 @@ module T::Props
110
110
  # this union to have no specific serde transform (the only reason
111
111
  # why Float has a special case is because round tripping through
112
112
  # JSON might normalize Floats to Integers)
113
- "T::Props::Utils.deep_clone_object(#{varname})"
113
+ "T::Props::Utils.deep_clone(#{varname})"
114
114
  end
115
115
  when T::Types::Intersection
116
- dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"
116
+ dynamic_fallback = "T::Props::Utils.deep_clone(#{varname})"
117
117
 
118
118
  # Transformations for any members of the intersection type where we
119
119
  # know what we need to do and did not have to fall back to the
@@ -147,7 +147,7 @@ module T::Props
147
147
  when T::Types::Enum
148
148
  generate(T::Utils.lift_enum(type), mode, varname)
149
149
  else
150
- "T::Props::Utils.deep_clone_object(#{varname})"
150
+ "T::Props::Utils.deep_clone(#{varname})"
151
151
  end
152
152
  end
153
153
 
@@ -8,6 +8,10 @@ module T::Props
8
8
 
9
9
  SetterProc = T.type_alias { T.proc.params(val: T.untyped).void }
10
10
  ValueValidationProc = T.type_alias { T.proc.params(val: T.untyped).void }
11
+ # Same validation/assignment as SetterProc, but takes the instance
12
+ # explicitly so per-prop construction/prop_set paths skip the
13
+ # self-rebinding instance_exec dispatch.
14
+ BoundSetterProc = T.type_alias { T.proc.params(instance: T.untyped, val: T.untyped).void }
11
15
  ValidateProc = T.type_alias { T.proc.params(prop: Symbol, value: T.untyped).void }
12
16
 
13
17
  sig do
@@ -16,7 +20,7 @@ module T::Props
16
20
  prop: Symbol,
17
21
  rules: T::Hash[Symbol, T.untyped]
18
22
  )
19
- .returns([SetterProc, ValueValidationProc])
23
+ .returns([SetterProc, ValueValidationProc, BoundSetterProc])
20
24
  .checked(:never)
21
25
  end
22
26
  def self.build_setter_proc(klass, prop, rules)
@@ -56,7 +60,7 @@ module T::Props
56
60
  non_nil_type: T::Module[T.anything],
57
61
  klass: T.all(T::Module[T.anything], T::Props::ClassMethods),
58
62
  )
59
- .returns([SetterProc, ValueValidationProc])
63
+ .returns([SetterProc, ValueValidationProc, BoundSetterProc])
60
64
  end
61
65
  private_class_method def self.simple_non_nil_proc(prop, accessor_key, non_nil_type, klass)
62
66
  [
@@ -81,6 +85,19 @@ module T::Props
81
85
  )
82
86
  end
83
87
  end,
88
+ # The ivar set is unconditional, exactly as above: the value must
89
+ # still be set when call_validation_error_handler does not raise.
90
+ proc do |instance, val|
91
+ unless val.is_a?(non_nil_type)
92
+ T::Props::Private::SetterFactory.raise_pretty_error(
93
+ klass,
94
+ prop,
95
+ T::Utils.coerce(non_nil_type),
96
+ val,
97
+ )
98
+ end
99
+ instance.instance_variable_set(accessor_key, val)
100
+ end,
84
101
  ]
85
102
  end
86
103
 
@@ -92,7 +109,7 @@ module T::Props
92
109
  klass: T.all(T::Module[T.anything], T::Props::ClassMethods),
93
110
  validate: T.nilable(ValidateProc)
94
111
  )
95
- .returns([SetterProc, ValueValidationProc])
112
+ .returns([SetterProc, ValueValidationProc, BoundSetterProc])
96
113
  end
97
114
  private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
98
115
  [
@@ -131,6 +148,21 @@ module T::Props
131
148
  )
132
149
  end
133
150
  end,
151
+ # The ivar set is unconditional, exactly as above: the value must
152
+ # still be set when call_validation_error_handler does not raise.
153
+ proc do |instance, val|
154
+ if non_nil_type.recursively_valid?(val)
155
+ validate&.call(prop, val)
156
+ else
157
+ T::Props::Private::SetterFactory.raise_pretty_error(
158
+ klass,
159
+ prop,
160
+ non_nil_type,
161
+ val,
162
+ )
163
+ end
164
+ instance.instance_variable_set(accessor_key, val)
165
+ end,
134
166
  ]
135
167
  end
136
168
 
@@ -141,7 +173,7 @@ module T::Props
141
173
  non_nil_type: T::Module[T.anything],
142
174
  klass: T.all(T::Module[T.anything], T::Props::ClassMethods),
143
175
  )
144
- .returns([SetterProc, ValueValidationProc])
176
+ .returns([SetterProc, ValueValidationProc, BoundSetterProc])
145
177
  end
146
178
  private_class_method def self.simple_nilable_proc(prop, accessor_key, non_nil_type, klass)
147
179
  [
@@ -166,6 +198,19 @@ module T::Props
166
198
  )
167
199
  end
168
200
  end,
201
+ # The ivar set is unconditional, exactly as above: the value must
202
+ # still be set when call_validation_error_handler does not raise.
203
+ proc do |instance, val|
204
+ unless val.nil? || val.is_a?(non_nil_type)
205
+ T::Props::Private::SetterFactory.raise_pretty_error(
206
+ klass,
207
+ prop,
208
+ T::Utils.coerce(non_nil_type),
209
+ val,
210
+ )
211
+ end
212
+ instance.instance_variable_set(accessor_key, val)
213
+ end,
169
214
  ]
170
215
  end
171
216
 
@@ -177,7 +222,7 @@ module T::Props
177
222
  klass: T.all(T::Module[T.anything], T::Props::ClassMethods),
178
223
  validate: T.nilable(ValidateProc),
179
224
  )
180
- .returns([SetterProc, ValueValidationProc])
225
+ .returns([SetterProc, ValueValidationProc, BoundSetterProc])
181
226
  end
182
227
  private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
183
228
  [
@@ -220,6 +265,24 @@ module T::Props
220
265
  )
221
266
  end
222
267
  end,
268
+ # Branch structure (including the set-after-soft-error in the
269
+ # invalid arm) replicated exactly from the first proc above.
270
+ proc do |instance, val|
271
+ if val.nil?
272
+ instance.instance_variable_set(accessor_key, nil)
273
+ elsif non_nil_type.recursively_valid?(val)
274
+ validate&.call(prop, val)
275
+ instance.instance_variable_set(accessor_key, val)
276
+ else
277
+ T::Props::Private::SetterFactory.raise_pretty_error(
278
+ klass,
279
+ prop,
280
+ non_nil_type,
281
+ val,
282
+ )
283
+ instance.instance_variable_set(accessor_key, val)
284
+ end
285
+ end,
223
286
  ]
224
287
  end
225
288
 
@@ -5,20 +5,48 @@ module T::Props::Utils
5
5
  # Deep copy an object. The object must consist of Ruby primitive
6
6
  # types and Hashes and Arrays.
7
7
  def self.deep_clone_object(what, freeze: false)
8
+ freeze ? deep_clone_freeze(what) : deep_clone(what)
9
+ end
10
+
11
+ # deep_clone_object with freeze: false. Kept kwarg-free, with String (the
12
+ # most common serialized scalar) tested first: this is what generated
13
+ # serializers/deserializers emit for dynamic fallbacks, so it runs per
14
+ # element of every untyped container prop.
15
+ def self.deep_clone(what)
16
+ case what
17
+ when String
18
+ what.clone
19
+ when true, false, Symbol, NilClass, Numeric
20
+ what
21
+ when Array
22
+ what.map { |v| deep_clone(v) }
23
+ when Hash
24
+ h = what.class.new
25
+ what.each_pair do |k, v|
26
+ h[k] = deep_clone(v)
27
+ end
28
+ h
29
+ when Regexp
30
+ what.dup
31
+ when T::Enum
32
+ what
33
+ else
34
+ what.clone
35
+ end
36
+ end
37
+
38
+ # deep_clone_object with freeze: true.
39
+ def self.deep_clone_freeze(what)
8
40
  result = case what
9
- when true
10
- true
11
- when false
12
- false
13
- when Symbol, NilClass, Numeric
41
+ when true, false, Symbol, NilClass, Numeric
14
42
  what
15
43
  when Array
16
- what.map { |v| deep_clone_object(v, freeze: freeze) }
44
+ what.map { |v| deep_clone_freeze(v) }
17
45
  when Hash
18
46
  h = what.class.new
19
- what.each do |k, v|
20
- k.freeze if freeze
21
- h[k] = deep_clone_object(v, freeze: freeze)
47
+ what.each_pair do |k, v|
48
+ k.freeze
49
+ h[k] = deep_clone_freeze(v)
22
50
  end
23
51
  h
24
52
  when Regexp
@@ -28,7 +56,7 @@ module T::Props::Utils
28
56
  else
29
57
  what.clone
30
58
  end
31
- freeze ? result.freeze : result
59
+ result.freeze
32
60
  end
33
61
 
34
62
  # The prop_rules indicate whether we should check for reading a nil value for the prop/field.
@@ -5,9 +5,14 @@ module T::Props::WeakConstructor
5
5
  include T::Props::Optional
6
6
  extend T::Sig
7
7
 
8
+ # Shared default so zero-arg construction doesn't allocate a fresh Hash;
9
+ # the construct_props_* methods only ever read from `hash`.
10
+ EMPTY_HASH = T.let({}.freeze, T::Hash[Symbol, T.untyped])
11
+ private_constant :EMPTY_HASH
12
+
8
13
  # checked(:never) - O(runtime object construction)
9
14
  sig { params(hash: T::Hash[Symbol, T.untyped]).void.checked(:never) }
10
- def initialize(hash={})
15
+ def initialize(hash=EMPTY_HASH)
11
16
  decorator = self.class.decorator
12
17
 
13
18
  hash_keys_matching_props = decorator.construct_props_with_defaults(self, hash) +
@@ -33,9 +38,9 @@ module T::Props::WeakConstructor::DecoratorMethods
33
38
  # Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
34
39
  # and therefore allocates for each entry.
35
40
  result = 0
36
- props_without_defaults&.each_pair do |p, setter_proc|
41
+ props_without_defaults&.each_pair do |p, bound_setter|
37
42
  if hash.key?(p)
38
- instance.instance_exec(hash[p], &setter_proc)
43
+ bound_setter.call(instance, hash[p])
39
44
  result += 1
40
45
  end
41
46
  end
@@ -56,7 +61,7 @@ module T::Props::WeakConstructor::DecoratorMethods
56
61
  result = 0
57
62
  props_with_defaults&.each_pair do |p, default_struct|
58
63
  if hash.key?(p)
59
- instance.instance_exec(hash[p], &default_struct.setter_proc)
64
+ default_struct.bound_setter_proc.call(instance, hash[p])
60
65
  result += 1
61
66
  else
62
67
  default_struct.set_default(instance)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.13299
4
+ version: 0.6.13300
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe