sorbet-runtime 0.5.5413 → 0.5.5417

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee0cee600d0c6c8dffef881a35e510ab5e4247e0d1e156f6d1ee83b7d626af4b
4
- data.tar.gz: e3f5c3dea96103665ac92c3b0519f47352d89aa4822acae16f96beb429ab548f
3
+ metadata.gz: c236407d857c6e24b8e71d59eb7ad8ec51040e48c0f24c56cee6d1385a9a92ab
4
+ data.tar.gz: 7a8ad4e57f69bfbff85589e862057345cebbb63c5ec5ed74268e902982cd6731
5
5
  SHA512:
6
- metadata.gz: 7b035657a30fe89c2f598c21f07896e476217ea8708ee3569ff1c45b54dd9df3317547efa10137889a15fe01aa712f414b2a30ccdb636f51c01fb18a52a57f12
7
- data.tar.gz: b403812e28bc276d8514c0cfa1fabc856a2027b085bd78460cf018d5eb0c5f6383e066cb23976eef909c9e7121eb01745b4f702f24ae86cf10b426762ab0974d
6
+ metadata.gz: e897902e9b86b94cb9c59ea2f07f5de20499bf0f178f4b2c0820df1a047c8010120c1fac1a56f977a0ec0b1493051898bfc648c9e54ce9379fc079870e747789
7
+ data.tar.gz: 7321f38a27966c9cfbfb66b8af11d4f2be9ebbb0b2d80cc5becafd516a2664c1167f57b102aa8a71b5deccd90a645c06237e4018429a0e7ffb5d46e3717e95da
@@ -96,12 +96,18 @@ require_relative 'types/enum'
96
96
  # Props that run sigs statically so have to be after all the others :(
97
97
  require_relative 'types/props/private/setter_factory'
98
98
  require_relative 'types/props/private/apply_default'
99
+ require_relative 'types/props/has_lazily_specialized_methods'
99
100
  require_relative 'types/props/optional'
100
101
  require_relative 'types/props/weak_constructor'
101
102
  require_relative 'types/props/constructor'
102
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'
103
107
  require_relative 'types/props/serializable'
104
108
  require_relative 'types/props/type_validation'
109
+ require_relative 'types/props/private/parser'
110
+ require_relative 'types/props/generated_code_validation'
105
111
 
106
112
  require_relative 'types/struct'
107
113
 
@@ -80,5 +80,17 @@ module T::Props
80
80
  scalar_type?(val)
81
81
  end
82
82
  end
83
+
84
+ def self.checked_serialize(type, instance)
85
+ val = type.serialize(instance)
86
+ unless valid_serialization?(val, type)
87
+ msg = "#{type} did not serialize to a valid scalar type. It became a: #{val.class}"
88
+ if val.is_a?(Hash)
89
+ msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
90
+ end
91
+ raise T::Props::InvalidValueError.new(msg)
92
+ end
93
+ val
94
+ end
83
95
  end
84
96
  end
@@ -230,10 +230,12 @@ class T::Props::Decorator
230
230
  nil
231
231
  end
232
232
 
233
+ SAFE_NAME = /\A[A-Za-z_][A-Za-z0-9_-]*\z/
234
+
233
235
  # Used to validate both prop names and serialized forms
234
236
  sig {params(name: T.any(Symbol, String)).void}
235
237
  private def validate_prop_name(name)
236
- if !name.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/)
238
+ if !name.match?(SAFE_NAME)
237
239
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
238
240
  end
239
241
  end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Props
5
+ # Helper to validate generated code, to mitigate security concerns around
6
+ # `class_eval`. Not called by default; the expectation is this will be used
7
+ # in a test iterating over all T::Props::Serializable subclasses.
8
+ #
9
+ # We validate the exact expected structure of the generated methods as far
10
+ # as we can, and then where cloning produces an arbitrarily nested structure,
11
+ # we just validate a lack of side effects.
12
+ module GeneratedCodeValidation
13
+ extend Private::Parse
14
+
15
+ class ValidationError < RuntimeError; end
16
+
17
+ def self.validate_deserialize(source)
18
+ parsed = parse(source)
19
+
20
+ # def %<name>(hash)
21
+ # ...
22
+ # end
23
+ assert_equal(:def, parsed.type)
24
+ name, args, body = parsed.children
25
+ assert_equal(:__t_props_generated_deserialize, name)
26
+ assert_equal(s(:args, s(:arg, :hash)), args)
27
+
28
+ assert_equal(:begin, body.type)
29
+ init, *prop_clauses, ret = body.children
30
+
31
+ # found = %<prop_count>
32
+ # ...
33
+ # found
34
+ assert_equal(:lvasgn, init.type)
35
+ init_name, init_val = init.children
36
+ assert_equal(:found, init_name)
37
+ assert_equal(:int, init_val.type)
38
+ assert_equal(s(:lvar, :found), ret)
39
+
40
+ prop_clauses.each_with_index do |clause, i|
41
+ if i % 2 == 0
42
+ validate_deserialize_hash_read(clause)
43
+ else
44
+ validate_deserialize_ivar_set(clause)
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.validate_serialize(source)
50
+ parsed = parse(source)
51
+
52
+ # def %<name>(strict)
53
+ # ...
54
+ # end
55
+ assert_equal(:def, parsed.type)
56
+ name, args, body = parsed.children
57
+ assert_equal(:__t_props_generated_serialize, name)
58
+ assert_equal(s(:args, s(:arg, :strict)), args)
59
+
60
+ assert_equal(:begin, body.type)
61
+ init, *prop_clauses, ret = body.children
62
+
63
+ # h = {}
64
+ # ...
65
+ # h
66
+ assert_equal(s(:lvasgn, :h, s(:hash)), init)
67
+ assert_equal(s(:lvar, :h), ret)
68
+
69
+ prop_clauses.each do |clause|
70
+ validate_serialize_clause(clause)
71
+ end
72
+ end
73
+
74
+ private_class_method def self.validate_serialize_clause(clause)
75
+ assert_equal(:if, clause.type)
76
+ condition, if_body, else_body = clause.children
77
+
78
+ # if @%<accessor_key>.nil?
79
+ assert_equal(:send, condition.type)
80
+ receiver, method = condition.children
81
+ assert_equal(:ivar, receiver.type)
82
+ assert_equal(:nil?, method)
83
+
84
+ unless if_body.nil?
85
+ # required_prop_missing_from_serialize(%<prop>) if strict
86
+ assert_equal(:if, if_body.type)
87
+ if_strict_condition, if_strict_body, if_strict_else = if_body.children
88
+ assert_equal(s(:lvar, :strict), if_strict_condition)
89
+ assert_equal(:send, if_strict_body.type)
90
+ on_strict_receiver, on_strict_method, on_strict_arg = if_strict_body.children
91
+ assert_equal(nil, on_strict_receiver)
92
+ assert_equal(:required_prop_missing_from_serialize, on_strict_method)
93
+ assert_equal(:sym, on_strict_arg.type)
94
+ assert_equal(nil, if_strict_else)
95
+ end
96
+
97
+ # h[%<serialized_form>] = ...
98
+ assert_equal(:send, else_body.type)
99
+ receiver, method, h_key, h_val = else_body.children
100
+ assert_equal(s(:lvar, :h), receiver)
101
+ assert_equal(:[]=, method)
102
+ assert_equal(:str, h_key.type)
103
+
104
+ validate_lack_of_side_effects(h_val, whitelisted_methods_for_serialize)
105
+ end
106
+
107
+ private_class_method def self.validate_deserialize_hash_read(clause)
108
+ # val = hash[%<serialized_form>s]
109
+
110
+ assert_equal(:lvasgn, clause.type)
111
+ name, val = clause.children
112
+ assert_equal(:val, name)
113
+ assert_equal(:send, val.type)
114
+ receiver, method, arg = val.children
115
+ assert_equal(s(:lvar, :hash), receiver)
116
+ assert_equal(:[], method)
117
+ assert_equal(:str, arg.type)
118
+ end
119
+
120
+ private_class_method def self.validate_deserialize_ivar_set(clause)
121
+ # %<accessor_key>s = if val.nil?
122
+ # found -= 1 unless hash.key?(%<serialized_form>s)
123
+ # %<nil_handler>s
124
+ # else
125
+ # %<serialized_val>s
126
+ # end
127
+
128
+ assert_equal(:ivasgn, clause.type)
129
+ ivar_name, deser_val = clause.children
130
+ unless ivar_name.is_a?(Symbol)
131
+ raise ValidationError.new("Unexpected ivar: #{ivar_name}")
132
+ end
133
+
134
+ assert_equal(:if, deser_val.type)
135
+ condition, if_body, else_body = deser_val.children
136
+ assert_equal(s(:send, s(:lvar, :val), :nil?), condition)
137
+
138
+ assert_equal(:begin, if_body.type)
139
+ update_found, handle_nil = if_body.children
140
+ assert_equal(:if, update_found.type)
141
+ found_condition, found_if_body, found_else_body = update_found.children
142
+ assert_equal(:send, found_condition.type)
143
+ receiver, method, arg = found_condition.children
144
+ assert_equal(s(:lvar, :hash), receiver)
145
+ assert_equal(:key?, method)
146
+ assert_equal(:str, arg.type)
147
+ assert_equal(nil, found_if_body)
148
+ assert_equal(s(:op_asgn, s(:lvasgn, :found), :-, s(:int, 1)), found_else_body)
149
+
150
+ validate_deserialize_handle_nil(handle_nil)
151
+ validate_lack_of_side_effects(else_body, whitelisted_methods_for_deserialize)
152
+ end
153
+
154
+ private_class_method def self.validate_deserialize_handle_nil(node)
155
+ case node.type
156
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil
157
+ # Primitives are safe
158
+ when :send
159
+ receiver, method, arg = node.children
160
+ if receiver.nil?
161
+ # required_prop_missing_from_deserialize(%<prop>)
162
+ assert_equal(:required_prop_missing_from_deserialize, method)
163
+ assert_equal(:sym, arg.type)
164
+ elsif receiver == self_class_decorator
165
+ # self.class.decorator.raise_nil_deserialize_error(%<serialized_form>)
166
+ assert_equal(:raise_nil_deserialize_error, method)
167
+ assert_equal(:str, arg.type)
168
+ elsif method == :default
169
+ # self.class.decorator.props_with_defaults.fetch(%<prop>).default
170
+ assert_equal(:send, receiver.type)
171
+ inner_receiver, inner_method, inner_arg = receiver.children
172
+ assert_equal(
173
+ s(:send, self_class_decorator, :props_with_defaults),
174
+ inner_receiver,
175
+ )
176
+ assert_equal(:fetch, inner_method)
177
+ assert_equal(:sym, inner_arg.type)
178
+ else
179
+ raise ValidationError.new("Unexpected receiver in nil handler: #{node.inspect}")
180
+ end
181
+ else
182
+ raise ValidationError.new("Unexpected nil handler: #{node.inspect}")
183
+ end
184
+ end
185
+
186
+ private_class_method def self.self_class_decorator
187
+ @self_class_decorator ||= s(:send, s(:send, s(:self), :class), :decorator).freeze
188
+ end
189
+
190
+ private_class_method def self.validate_lack_of_side_effects(node, whitelisted_methods_by_receiver_type)
191
+ case node.type
192
+ when :const
193
+ # This is ok, because we'll have validated what method has been called
194
+ # if applicable
195
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :self
196
+ # Primitives & self are ok
197
+ when :lvar, :arg, :ivar
198
+ # Reading local & instance variables & arguments is ok
199
+ unless node.children.all? {|c| c.is_a?(Symbol)}
200
+ raise ValidationError.new("Unexpected child for #{node.type}: #{node.inspect}")
201
+ end
202
+ when :args, :mlhs, :block, :begin, :if
203
+ # Blocks etc are read-only if their contents are read-only
204
+ node.children.each {|c| validate_lack_of_side_effects(c, whitelisted_methods_by_receiver_type) if c}
205
+ when :send
206
+ # Sends are riskier so check a whitelist
207
+ receiver, method, *args = node.children
208
+ if receiver
209
+ if receiver.type == :send
210
+ key = receiver
211
+ else
212
+ key = receiver.type
213
+ validate_lack_of_side_effects(receiver, whitelisted_methods_by_receiver_type)
214
+ end
215
+
216
+ if !whitelisted_methods_by_receiver_type[key]&.include?(method)
217
+ raise ValidationError.new("Unexpected method #{method} called on #{receiver.inspect}")
218
+ end
219
+ end
220
+ args.each do |arg|
221
+ validate_lack_of_side_effects(arg, whitelisted_methods_by_receiver_type)
222
+ end
223
+ else
224
+ raise ValidationError.new("Unexpected node type #{node.type}: #{node.inspect}")
225
+ end
226
+ end
227
+
228
+ private_class_method def self.assert_equal(expected, actual)
229
+ if expected != actual
230
+ raise ValidationError.new("Expected #{expected}, got #{actual}")
231
+ end
232
+ end
233
+
234
+ # Method calls generated by SerdeTransform
235
+ private_class_method def self.whitelisted_methods_for_serialize
236
+ @whitelisted_methods_for_serialize ||= {
237
+ :lvar => %i{dup map transform_values transform_keys each_with_object nil? []= serialize},
238
+ :ivar => %i{dup map transform_values transform_keys each_with_object serialize},
239
+ :const => %i{checked_serialize deep_clone_object},
240
+ }
241
+ end
242
+
243
+ # Method calls generated by SerdeTransform
244
+ private_class_method def self.whitelisted_methods_for_deserialize
245
+ @whitelisted_methods_for_deserialize ||= {
246
+ :lvar => %i{dup map transform_values transform_keys each_with_object nil? []=},
247
+ :const => %i{deserialize from_hash deep_clone_object},
248
+ }
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props
5
+
6
+ # Helper for generating methods that replace themselves with a specialized
7
+ # version on first use. The main use case is when we want to generate a
8
+ # method using the full set of props on a class; we can't do that during
9
+ # prop definition because we have no way of knowing whether we are defining
10
+ # the last prop.
11
+ #
12
+ # See go/M8yrvzX2 (Stripe-internal) for discussion of security considerations.
13
+ # In outline, while `class_eval` is a bit scary, we believe that as long as
14
+ # all inputs are defined in version control (and this is enforced by calling
15
+ # `disable_lazy_evaluation!` appropriately), risk isn't significantly higher
16
+ # than with build-time codegen.
17
+ module HasLazilySpecializedMethods
18
+ extend T::Sig
19
+
20
+ class SourceEvaluationDisabled < RuntimeError
21
+ def initialize
22
+ super("Evaluation of lazily-defined methods is disabled")
23
+ end
24
+ end
25
+
26
+ # Disable any future evaluation of lazily-defined methods.
27
+ #
28
+ # This is intended to be called after startup but before interacting with
29
+ # the outside world, to limit attack surface for our `class_eval` use.
30
+ sig {void}
31
+ def self.disable_lazy_evaluation!
32
+ @lazy_evaluation_disabled ||= true
33
+ end
34
+
35
+ sig {returns(T::Boolean)}
36
+ def self.lazy_evaluation_enabled?
37
+ !@lazy_evaluation_disabled
38
+ end
39
+
40
+ module DecoratorMethods
41
+ extend T::Sig
42
+
43
+ sig {returns(T::Hash[Symbol, T.proc.returns(String)]).checked(:never)}
44
+ private def lazily_defined_methods
45
+ @lazily_defined_methods ||= {}
46
+ end
47
+
48
+ sig {params(name: Symbol).void}
49
+ private def eval_lazily_defined_method!(name)
50
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
51
+ raise SourceEvaluationDisabled.new
52
+ end
53
+
54
+ source = lazily_defined_methods.fetch(name).call
55
+
56
+ cls = decorated_class
57
+ cls.class_eval(source.to_s)
58
+ cls.send(:private, name)
59
+ end
60
+
61
+ sig {params(name: Symbol, blk: T.proc.returns(String)).void}
62
+ private def enqueue_lazy_method_definition!(name, &blk)
63
+ lazily_defined_methods[name] = blk
64
+
65
+ cls = decorated_class
66
+ cls.define_method(name) do |*args|
67
+ self.class.decorator.send(:eval_lazily_defined_method!, name)
68
+ send(name, *args)
69
+ end
70
+ cls.send(:private, name)
71
+ end
72
+
73
+ sig {void}
74
+ def eagerly_define_lazy_methods!
75
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
76
+ raise SourceEvaluationDisabled.new
77
+ elsif lazily_defined_methods.empty?
78
+ return
79
+ end
80
+
81
+ source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
82
+
83
+ cls = decorated_class
84
+ cls.class_eval(source)
85
+ lazily_defined_methods.keys.each {|name| cls.send(:private, name)}
86
+ lazily_defined_methods.clear
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module Private
6
+
7
+ # Generates a specialized `deserialize` implementation for a subclass of
8
+ # T::Props::Serializable.
9
+ #
10
+ # The basic idea is that we analyze the props and for each prop, generate
11
+ # the simplest possible logic as a block of Ruby source, so that we don't
12
+ # pay the cost of supporting types like T:::Hash[CustomType, SubstructType]
13
+ # when deserializing a simple Integer. Then we join those together,
14
+ # with a little shared logic to be able to detect when we get input keys
15
+ # that don't match any prop.
16
+ module DeserializerGenerator
17
+ extend T::Sig
18
+
19
+ # Generate a method that takes a T::Hash[String, T.untyped] representing
20
+ # serialized props, sets instance variables for each prop found in the
21
+ # input, and returns the count of we props set (which we can use to check
22
+ # for unexpected input keys with minimal effect on the fast path).
23
+ sig do
24
+ params(
25
+ props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
26
+ defaults: T::Hash[Symbol, T::Props::Private::ApplyDefault],
27
+ )
28
+ .returns(String)
29
+ .checked(:never)
30
+ end
31
+ def self.generate(props, defaults)
32
+ stored_props = props.reject {|_, rules| rules[:dont_store]}
33
+ parts = stored_props.map do |prop, rules|
34
+ # All of these strings should already be validated (directly or
35
+ # indirectly) in `validate_prop_name`, so we don't bother with a nice
36
+ # error message, but we double check here to prevent a refactoring
37
+ # from introducing a security vulnerability.
38
+ raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
39
+
40
+ hash_key = rules.fetch(:serialized_form)
41
+ raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
42
+
43
+ ivar_name = rules.fetch(:accessor_key).to_s
44
+ raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
45
+
46
+ transformed_val = SerdeTransform.generate(
47
+ T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
48
+ SerdeTransform::Mode::DESERIALIZE,
49
+ 'val'
50
+ ) || 'val'
51
+
52
+ nil_handler = generate_nil_handler(
53
+ prop: prop,
54
+ serialized_form: hash_key,
55
+ default: defaults[prop],
56
+ nilable_type: T::Props::Utils.optional_prop?(rules),
57
+ raise_on_nil_write: !!rules[:raise_on_nil_write],
58
+ )
59
+
60
+ <<~RUBY
61
+ val = hash[#{hash_key.inspect}]
62
+ #{ivar_name} = if val.nil?
63
+ found -= 1 unless hash.key?(#{hash_key.inspect})
64
+ #{nil_handler}
65
+ else
66
+ #{transformed_val}
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ <<~RUBY
72
+ def __t_props_generated_deserialize(hash)
73
+ found = #{stored_props.size}
74
+ #{parts.join("\n\n")}
75
+ found
76
+ end
77
+ RUBY
78
+ end
79
+
80
+ # This is very similar to what we do in ApplyDefault, but has a few
81
+ # key differences that mean we don't just re-use the code:
82
+ #
83
+ # 1. Where the logic in construction is that we generate a default
84
+ # if & only if the prop key isn't present in the input, here we'll
85
+ # generate a default even to override an explicit nil, but only
86
+ # if the prop is actually required.
87
+ # 2. Since we're generating raw Ruby source, we can remove a layer
88
+ # of indirection for marginally better performance; this seems worth
89
+ # it for the common cases of literals and empty arrays/hashes.
90
+ # 3. We need to care about the distinction between `raise_on_nil_write`
91
+ # and actually non-nilable, where new-instance construction doesn't.
92
+ #
93
+ # So we fall back to ApplyDefault only when one of the cases just
94
+ # mentioned doesn't apply.
95
+ sig do
96
+ params(
97
+ prop: Symbol,
98
+ serialized_form: String,
99
+ default: T.nilable(ApplyDefault),
100
+ nilable_type: T::Boolean,
101
+ raise_on_nil_write: T::Boolean,
102
+ )
103
+ .returns(String)
104
+ .checked(:never)
105
+ end
106
+ private_class_method def self.generate_nil_handler(
107
+ prop:,
108
+ serialized_form:,
109
+ default:,
110
+ nilable_type:,
111
+ raise_on_nil_write:
112
+ )
113
+ if !nilable_type
114
+ case default
115
+ when NilClass
116
+ "self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"
117
+ when ApplyPrimitiveDefault
118
+ literal = default.default
119
+ case literal
120
+ when String, Integer, Symbol, Float, TrueClass, FalseClass, NilClass
121
+ literal.inspect
122
+ else
123
+ "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
124
+ end
125
+ when ApplyEmptyArrayDefault
126
+ '[]'
127
+ when ApplyEmptyHashDefault
128
+ '{}'
129
+ else
130
+ "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
131
+ end
132
+ elsif raise_on_nil_write
133
+ "required_prop_missing_from_deserialize(#{prop.inspect})"
134
+ else
135
+ 'nil'
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props
5
+ module Private
6
+ module Parse
7
+ def parse(source)
8
+ @current_ruby ||= require_parser(:CurrentRuby)
9
+ @current_ruby.parse(source)
10
+ end
11
+
12
+ def s(type, *children)
13
+ @node ||= require_parser(:AST, :Node)
14
+ @node.new(type, children)
15
+ end
16
+
17
+ private def require_parser(*constants)
18
+ # This is an optional dependency for sorbet-runtime in general,
19
+ # but is required here
20
+ require 'parser/current'
21
+
22
+ # Hack to work around the static checker thinking the constant is
23
+ # undefined
24
+ cls = Kernel.const_get(:Parser, true)
25
+ while (const = constants.shift)
26
+ cls = cls.const_get(const, false)
27
+ end
28
+ cls
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module Private
6
+ module SerdeTransform
7
+ extend T::Sig
8
+
9
+ class Mode < T::Enum
10
+ enums do
11
+ SERIALIZE = new
12
+ DESERIALIZE = new
13
+ end
14
+ end
15
+
16
+ NO_TRANSFORM_TYPES = T.let(
17
+ [TrueClass, FalseClass, NilClass, Symbol, String, Numeric].freeze,
18
+ T::Array[Module],
19
+ )
20
+ private_constant :NO_TRANSFORM_TYPES
21
+
22
+ sig do
23
+ params(
24
+ type: T.any(T::Types::Base, Module),
25
+ mode: Mode,
26
+ varname: String,
27
+ )
28
+ .returns(T.nilable(String))
29
+ .checked(:never)
30
+ end
31
+ def self.generate(type, mode, varname)
32
+ case type
33
+ when T::Types::TypedArray
34
+ inner = generate(type.type, mode, 'v')
35
+ if inner.nil?
36
+ "#{varname}.dup"
37
+ else
38
+ "#{varname}.map {|v| #{inner}}"
39
+ end
40
+ when T::Types::TypedSet
41
+ inner = generate(type.type, mode, 'v')
42
+ if inner.nil?
43
+ "#{varname}.dup"
44
+ else
45
+ "Set.new(#{varname}) {|v| #{inner}}"
46
+ end
47
+ when T::Types::TypedHash
48
+ keys = generate(type.keys, mode, 'k')
49
+ values = generate(type.values, mode, 'v')
50
+ if keys && values
51
+ "#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
52
+ elsif keys
53
+ "#{varname}.transform_keys {|k| #{keys}}"
54
+ elsif values
55
+ "#{varname}.transform_values {|v| #{values}}"
56
+ else
57
+ "#{varname}.dup"
58
+ end
59
+ when T::Types::Simple
60
+ raw = type.raw_type
61
+ if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
62
+ nil
63
+ elsif raw < T::Props::Serializable
64
+ handle_serializable_subtype(varname, raw, mode)
65
+ elsif raw.singleton_class < T::Props::CustomType
66
+ handle_custom_type(varname, T.unsafe(raw), mode)
67
+ else
68
+ "T::Props::Utils.deep_clone_object(#{varname})"
69
+ end
70
+ when T::Types::Union
71
+ non_nil_type = T::Utils.unwrap_nilable(type)
72
+ if non_nil_type
73
+ inner = generate(non_nil_type, mode, varname)
74
+ if inner.nil?
75
+ nil
76
+ else
77
+ "#{varname}.nil? ? nil : #{inner}"
78
+ end
79
+ else
80
+ "T::Props::Utils.deep_clone_object(#{varname})"
81
+ end
82
+ when T::Types::Enum
83
+ generate(T::Utils.lift_enum(type), mode, varname)
84
+ else
85
+ if type.singleton_class < T::Props::CustomType
86
+ # Sometimes this comes wrapped in a T::Types::Simple and sometimes not
87
+ handle_custom_type(varname, T.unsafe(type), mode)
88
+ else
89
+ "T::Props::Utils.deep_clone_object(#{varname})"
90
+ end
91
+ end
92
+ end
93
+
94
+ sig {params(varname: String, type: Module, mode: Mode).returns(String).checked(:never)}
95
+ private_class_method def self.handle_serializable_subtype(varname, type, mode)
96
+ case mode
97
+ when Mode::SERIALIZE
98
+ "#{varname}.serialize(strict)"
99
+ when Mode::DESERIALIZE
100
+ type_name = T.must(module_name(type))
101
+ "#{type_name}.from_hash(#{varname})"
102
+ else
103
+ T.absurd(mode)
104
+ end
105
+ end
106
+
107
+ sig {params(varname: String, type: Module, mode: Mode).returns(String).checked(:never)}
108
+ private_class_method def self.handle_custom_type(varname, type, mode)
109
+ case mode
110
+ when Mode::SERIALIZE
111
+ type_name = T.must(module_name(type))
112
+ "T::Props::CustomType.checked_serialize(#{type_name}, #{varname})"
113
+ when Mode::DESERIALIZE
114
+ type_name = T.must(module_name(type))
115
+ "#{type_name}.deserialize(#{varname})"
116
+ else
117
+ T.absurd(mode)
118
+ end
119
+ end
120
+
121
+ # Guard against overrides of `name` or `to_s`
122
+ MODULE_NAME = T.let(Module.instance_method(:name), UnboundMethod)
123
+ private_constant :MODULE_NAME
124
+
125
+ sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
126
+ private_class_method def self.module_name(type)
127
+ MODULE_NAME.bind(type).call
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module Private
6
+
7
+ # Generates a specialized `serialize` implementation for a subclass of
8
+ # T::Props::Serializable.
9
+ #
10
+ # The basic idea is that we analyze the props and for each prop, generate
11
+ # the simplest possible logic as a block of Ruby source, so that we don't
12
+ # pay the cost of supporting types like T:::Hash[CustomType, SubstructType]
13
+ # when serializing a simple Integer. Then we join those together,
14
+ # with a little shared logic to be able to detect when we get input keys
15
+ # that don't match any prop.
16
+ module SerializerGenerator
17
+ extend T::Sig
18
+
19
+ sig do
20
+ params(
21
+ props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
22
+ )
23
+ .returns(String)
24
+ .checked(:never)
25
+ end
26
+ def self.generate(props)
27
+ stored_props = props.reject {|_, rules| rules[:dont_store]}
28
+ parts = stored_props.map do |prop, rules|
29
+ # All of these strings should already be validated (directly or
30
+ # indirectly) in `validate_prop_name`, so we don't bother with a nice
31
+ # error message, but we double check here to prevent a refactoring
32
+ # from introducing a security vulnerability.
33
+ raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
34
+
35
+ hash_key = rules.fetch(:serialized_form)
36
+ raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
37
+
38
+ ivar_name = rules.fetch(:accessor_key).to_s
39
+ raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
40
+
41
+ transformed_val = SerdeTransform.generate(
42
+ T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
43
+ SerdeTransform::Mode::SERIALIZE,
44
+ ivar_name
45
+ ) || ivar_name
46
+
47
+ nil_asserter =
48
+ if rules[:fully_optional]
49
+ ''
50
+ else
51
+ "required_prop_missing_from_serialize(#{prop.inspect}) if strict"
52
+ end
53
+
54
+ # Don't serialize values that are nil to save space (both the
55
+ # nil value itself and the field name in the serialized BSON
56
+ # document)
57
+ <<~RUBY
58
+ if #{ivar_name}.nil?
59
+ #{nil_asserter}
60
+ else
61
+ h[#{hash_key.inspect}] = #{transformed_val}
62
+ end
63
+ RUBY
64
+ end
65
+
66
+ <<~RUBY
67
+ def __t_props_generated_serialize(strict)
68
+ h = {}
69
+ #{parts.join("\n\n")}
70
+ h
71
+ end
72
+ RUBY
73
+ end
74
+ end
75
+ end
76
+ end
77
+
@@ -16,96 +16,18 @@ module T::Props::Serializable
16
16
  # values.
17
17
  # @return [Hash] A serialization of this object.
18
18
  def serialize(strict=true)
19
- decorator = self.class.decorator
20
- h = {}
21
-
22
- decorator.props.each do |prop, rules|
23
- hkey = rules[:serialized_form]
24
-
25
- val = decorator.get(self, prop, rules)
26
-
27
- if val.nil? && strict && !rules[:fully_optional]
28
- # If the prop was already missing during deserialization, that means the application
29
- # code already had to deal with a nil value, which means we wouldn't be accomplishing
30
- # much by raising here (other than causing an unnecessary breakage).
31
- if self.required_prop_missing_from_deserialize?(prop)
32
- T::Configuration.log_info_handler(
33
- "chalk-odm: missing required property in serialize",
34
- prop: prop, class: self.class.name, id: decorator.get_id(self)
35
- )
36
- else
37
- raise T::Props::InvalidValueError.new("#{self.class.name}.#{prop} not set for non-optional prop")
38
- end
39
- end
40
-
41
- # Don't serialize values that are nil to save space (both the
42
- # nil value itself and the field name in the serialized BSON
43
- # document)
44
- next if rules[:dont_store] || val.nil?
45
-
46
- if rules[:serializable_subtype]
47
- if rules[:type_is_serializable]
48
- val = val.serialize(strict)
49
- elsif rules[:type_is_array_of_serializable]
50
- if (subtype = rules[:serializable_subtype]).is_a?(T::Props::CustomType)
51
- val = val.map {|el| el && subtype.serialize(el)}
52
- else
53
- val = val.map {|el| el && el.serialize(strict)}
54
- end
55
- elsif rules[:type_is_hash_of_serializable_values] && rules[:type_is_hash_of_custom_type_keys]
56
- key_subtype = rules[:serializable_subtype][:keys]
57
- value_subtype = rules[:serializable_subtype][:values]
58
- if value_subtype.is_a?(T::Props::CustomType)
59
- val = val.each_with_object({}) do |(key, value), result|
60
- result[key_subtype.serialize(key)] = value && value_subtype.serialize(value)
61
- end
62
- else
63
- val = val.each_with_object({}) do |(key, value), result|
64
- result[key_subtype.serialize(key)] = value && value.serialize(strict)
65
- end
66
- end
67
- elsif rules[:type_is_hash_of_serializable_values]
68
- value_subtype = rules[:serializable_subtype]
69
- if value_subtype.is_a?(T::Props::CustomType)
70
- val = val.transform_values {|v| v && value_subtype.serialize(v)}
71
- else
72
- val = val.transform_values {|v| v && v.serialize(strict)}
73
- end
74
- elsif rules[:type_is_hash_of_custom_type_keys]
75
- key_subtype = rules[:serializable_subtype]
76
- val = val.each_with_object({}) do |(key, value), result|
77
- result[key_subtype.serialize(key)] = value
78
- end
79
- end
80
- elsif rules[:type_is_custom_type]
81
- val = rules[:type].serialize(val)
82
-
83
- unless T::Props::CustomType.valid_serialization?(val, rules[:type])
84
- msg = "#{rules[:type]} did not serialize to a valid scalar type. It became a: #{val.class}"
85
- if val.is_a?(Hash)
86
- msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
87
- end
88
- raise T::Props::InvalidValueError.new(msg)
89
- end
90
- end
91
-
92
- needs_clone = rules[:type_needs_clone]
93
- if needs_clone
94
- if needs_clone == :shallow
95
- val = val.dup
96
- else
97
- val = T::Props::Utils.deep_clone_object(val)
98
- end
99
- end
100
-
101
- h[hkey] = val
102
- end
103
-
19
+ h = __t_props_generated_serialize(strict)
104
20
  h.merge!(@_extra_props) if @_extra_props
105
-
106
21
  h
107
22
  end
108
23
 
24
+ private def __t_props_generated_serialize(strict)
25
+ # No-op; will be overridden if there are any props.
26
+ #
27
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_serialize_source)`
28
+ {}
29
+ end
30
+
109
31
  # Populates the property values on this object with the values
110
32
  # from a hash. In general, prefer to use {.from_hash} to construct
111
33
  # a new instance, instead of loading into an existing instance.
@@ -117,105 +39,28 @@ module T::Props::Serializable
117
39
  # props on this instance.
118
40
  # @return [void]
119
41
  def deserialize(hash, strict=false)
120
- decorator = self.class.decorator
121
-
122
- matching_props = 0
123
-
124
- decorator.props.each do |p, rules|
125
- hkey = rules[:serialized_form]
126
- val = hash[hkey]
127
- if val.nil?
128
- if T::Props::Utils.required_prop?(rules)
129
- val = decorator.get_default(rules, self.class)
130
- if val.nil?
131
- msg = "Tried to deserialize a required prop from a nil value. It's "\
132
- "possible that a nil value exists in the database, so you should "\
133
- "provide a `default: or factory:` for this prop (see go/optional "\
134
- "for more details). If this is already the case, you probably "\
135
- "omitted a required prop from the `fields:` option when doing a "\
136
- "partial load."
137
- storytime = {prop: hkey, klass: self.class.name}
138
-
139
- # Notify the model owner if it exists, and always notify the API owner.
140
- begin
141
- if defined?(Opus) && defined?(Opus::Ownership) && decorator.decorated_class < Opus::Ownership
142
- T::Configuration.hard_assert_handler(
143
- msg,
144
- storytime: storytime,
145
- project: decorator.decorated_class.get_owner
146
- )
147
- end
148
- ensure
149
- T::Configuration.hard_assert_handler(msg, storytime: storytime)
150
- end
151
- end
152
- elsif rules[:need_nil_read_check]
153
- self.required_prop_missing_from_deserialize(p)
154
- end
155
-
156
- matching_props += 1 if hash.key?(hkey)
157
- else
158
- if (subtype = rules[:serializable_subtype])
159
- val =
160
- if rules[:type_is_array_of_serializable]
161
- if subtype.is_a?(T::Props::CustomType)
162
- val.map {|el| el && subtype.deserialize(el)}
163
- else
164
- val.map {|el| el && subtype.from_hash(el)}
165
- end
166
- elsif rules[:type_is_hash_of_serializable_values] && rules[:type_is_hash_of_custom_type_keys]
167
- key_subtype = subtype[:keys]
168
- values_subtype = subtype[:values]
169
- if values_subtype.is_a?(T::Props::CustomType)
170
- val.each_with_object({}) do |(key, value), result|
171
- result[key_subtype.deserialize(key)] = value && values_subtype.deserialize(value)
172
- end
173
- else
174
- val.each_with_object({}) do |(key, value), result|
175
- result[key_subtype.deserialize(key)] = value && values_subtype.from_hash(value)
176
- end
177
- end
178
- elsif rules[:type_is_hash_of_serializable_values]
179
- if subtype.is_a?(T::Props::CustomType)
180
- val.transform_values {|v| v && subtype.deserialize(v)}
181
- else
182
- val.transform_values {|v| v && subtype.from_hash(v)}
183
- end
184
- elsif rules[:type_is_hash_of_custom_type_keys]
185
- val.map do |key, value|
186
- [subtype.deserialize(key), value]
187
- end.to_h
188
- else
189
- subtype.from_hash(val)
190
- end
191
- elsif (needs_clone = rules[:type_needs_clone])
192
- val =
193
- if needs_clone == :shallow
194
- val.dup
195
- else
196
- T::Props::Utils.deep_clone_object(val)
197
- end
198
- elsif rules[:type_is_custom_type]
199
- val = rules[:type].deserialize(val)
42
+ hash_keys_matching_props = __t_props_generated_deserialize(hash)
43
+ if hash.size > hash_keys_matching_props
44
+ serialized_forms = self.class.decorator.prop_by_serialized_forms
45
+ extra = hash.reject {|k, _| serialized_forms.key?(k)}
46
+
47
+ # `extra` could still be empty here if the input matches a `dont_store` prop;
48
+ # historically, we just ignore those
49
+ if !extra.empty?
50
+ if strict
51
+ raise "Unknown properties for #{self.class.name}: #{extra.keys.inspect}"
52
+ else
53
+ @_extra_props = extra
200
54
  end
201
-
202
- matching_props += 1
203
55
  end
204
-
205
- self.instance_variable_set(rules[:accessor_key], val) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
206
56
  end
57
+ end
207
58
 
208
- # We compute extra_props this way specifically for performance
209
- if matching_props < hash.size
210
- pbsf = decorator.prop_by_serialized_forms
211
- h = hash.reject {|k, _| pbsf.key?(k)}
212
-
213
- if strict
214
- raise "Unknown properties for #{self.class.name}: #{h.keys.inspect}"
215
- else
216
- @_extra_props = h
217
- end
218
- end
59
+ private def __t_props_generated_deserialize(hash)
60
+ # No-op; will be overridden if there are any props.
61
+ #
62
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_deserialize_source)`
63
+ 0
219
64
  end
220
65
 
221
66
  # with() will clone the old object to the new object and merge the specified props to the new object.
@@ -254,14 +99,23 @@ module T::Props::Serializable
254
99
  new_val
255
100
  end
256
101
 
257
- # @return [T::Boolean] Was this property missing during deserialize?
258
- def required_prop_missing_from_deserialize?(prop)
259
- return false if @_required_props_missing_from_deserialize.nil?
260
- @_required_props_missing_from_deserialize.include?(prop)
102
+ # Asserts if this property is missing during strict serialize
103
+ private def required_prop_missing_from_serialize(prop)
104
+ if @_required_props_missing_from_deserialize&.include?(prop)
105
+ # If the prop was already missing during deserialization, that means the application
106
+ # code already had to deal with a nil value, which means we wouldn't be accomplishing
107
+ # much by raising here (other than causing an unnecessary breakage).
108
+ T::Configuration.log_info_handler(
109
+ "chalk-odm: missing required property in serialize",
110
+ prop: prop, class: self.class.name, id: self.class.decorator.get_id(self)
111
+ )
112
+ else
113
+ raise T::Props::InvalidValueError.new("#{self.class.name}.#{prop} not set for non-optional prop")
114
+ end
261
115
  end
262
116
 
263
- # @return Marks this property as missing during deserialize
264
- def required_prop_missing_from_deserialize(prop)
117
+ # Marks this property as missing during deserialize
118
+ private def required_prop_missing_from_deserialize(prop)
265
119
  @_required_props_missing_from_deserialize ||= Set[]
266
120
  @_required_props_missing_from_deserialize << prop
267
121
  nil
@@ -274,6 +128,7 @@ end
274
128
  # NB: This must stay in the same file where T::Props::Serializable is defined due to
275
129
  # T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
276
130
  module T::Props::Serializable::DecoratorMethods
131
+ include T::Props::HasLazilySpecializedMethods::DecoratorMethods
277
132
 
278
133
  VALID_RULE_KEYS = {dont_store: true, name: true, raise_on_nil_write: true}.freeze
279
134
  private_constant :VALID_RULE_KEYS
@@ -310,9 +165,45 @@ module T::Props::Serializable::DecoratorMethods
310
165
  rules[:serialized_form] = rules.fetch(:name, prop.to_s)
311
166
  res = super
312
167
  prop_by_serialized_forms[rules[:serialized_form]] = prop
168
+ enqueue_lazy_method_definition!(:__t_props_generated_serialize) {generate_serialize_source}
169
+ enqueue_lazy_method_definition!(:__t_props_generated_deserialize) {generate_deserialize_source}
313
170
  res
314
171
  end
315
172
 
173
+ private def generate_serialize_source
174
+ T::Props::Private::SerializerGenerator.generate(props)
175
+ end
176
+
177
+ private def generate_deserialize_source
178
+ T::Props::Private::DeserializerGenerator.generate(
179
+ props,
180
+ props_with_defaults || {},
181
+ )
182
+ end
183
+
184
+ def raise_nil_deserialize_error(hkey)
185
+ msg = "Tried to deserialize a required prop from a nil value. It's "\
186
+ "possible that a nil value exists in the database, so you should "\
187
+ "provide a `default: or factory:` for this prop (see go/optional "\
188
+ "for more details). If this is already the case, you probably "\
189
+ "omitted a required prop from the `fields:` option when doing a "\
190
+ "partial load."
191
+ storytime = {prop: hkey, klass: decorated_class.name}
192
+
193
+ # Notify the model owner if it exists, and always notify the API owner.
194
+ begin
195
+ if defined?(Opus) && defined?(Opus::Ownership) && decorated_class < Opus::Ownership
196
+ T::Configuration.hard_assert_handler(
197
+ msg,
198
+ storytime: storytime,
199
+ project: decorated_class.get_owner
200
+ )
201
+ end
202
+ ensure
203
+ T::Configuration.hard_assert_handler(msg, storytime: storytime)
204
+ end
205
+ end
206
+
316
207
  def prop_validate_definition!(name, cls, rules, type)
317
208
  result = super
318
209
 
data/lib/types/utils.rb CHANGED
@@ -151,6 +151,21 @@ module T::Utils
151
151
  "#{start_part}#{ellipsis}#{end_part}"
152
152
  end
153
153
 
154
+ def self.lift_enum(enum)
155
+ unless enum.is_a?(T::Types::Enum)
156
+ raise ArgumentError.new("#{enum.inspect} is not a T.enum")
157
+ end
158
+
159
+ classes = enum.values.map(&:class).uniq
160
+ if classes.empty?
161
+ T.untyped
162
+ elsif classes.length > 1
163
+ T::Types::Union.new(classes)
164
+ else
165
+ T::Types::Simple.new(classes.first)
166
+ end
167
+ end
168
+
154
169
  module Nilable
155
170
  # :is_union_type, T::Boolean: whether the type is an T::Types::Union type
156
171
  # :non_nilable_type, Class: if it is an T.nilable type, the corresponding underlying type; otherwise, nil.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5413
4
+ version: 0.5.5417
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-04 00:00:00.000000000 Z
11
+ date: 2020-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: parser
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.7'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
97
111
  description: Sorbet's runtime type checking component
98
112
  email:
99
113
  executables: []
@@ -137,10 +151,16 @@ files:
137
151
  - lib/types/props/custom_type.rb
138
152
  - lib/types/props/decorator.rb
139
153
  - lib/types/props/errors.rb
154
+ - lib/types/props/generated_code_validation.rb
155
+ - lib/types/props/has_lazily_specialized_methods.rb
140
156
  - lib/types/props/optional.rb
141
157
  - lib/types/props/plugin.rb
142
158
  - lib/types/props/pretty_printable.rb
143
159
  - lib/types/props/private/apply_default.rb
160
+ - lib/types/props/private/deserializer_generator.rb
161
+ - lib/types/props/private/parser.rb
162
+ - lib/types/props/private/serde_transform.rb
163
+ - lib/types/props/private/serializer_generator.rb
144
164
  - lib/types/props/private/setter_factory.rb
145
165
  - lib/types/props/serializable.rb
146
166
  - lib/types/props/type_validation.rb