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,268 @@
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.even?
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
+
152
+ if else_body.type == :kwbegin
153
+ rescue_expression, = else_body.children
154
+ assert_equal(:rescue, rescue_expression.type)
155
+
156
+ try, rescue_body = rescue_expression.children
157
+ validate_lack_of_side_effects(try, whitelisted_methods_for_deserialize)
158
+
159
+ assert_equal(:resbody, rescue_body.type)
160
+ exceptions, assignment, handler = rescue_body.children
161
+ assert_equal(:array, exceptions.type)
162
+ exceptions.children.each {|c| assert_equal(:const, c.type)}
163
+ assert_equal(:lvasgn, assignment.type)
164
+ assert_equal([:e], assignment.children)
165
+ validate_lack_of_side_effects(handler, whitelisted_methods_for_deserialize)
166
+ else
167
+ validate_lack_of_side_effects(else_body, whitelisted_methods_for_deserialize)
168
+ end
169
+ end
170
+
171
+ private_class_method def self.validate_deserialize_handle_nil(node)
172
+ case node.type
173
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :const # rubocop:disable Lint/BooleanSymbol
174
+ # Primitives and constants are safe
175
+ when :send
176
+ receiver, method, arg = node.children
177
+ if receiver.nil?
178
+ # required_prop_missing_from_deserialize(%<prop>)
179
+ assert_equal(:required_prop_missing_from_deserialize, method)
180
+ assert_equal(:sym, arg.type)
181
+ elsif receiver == self_class_decorator
182
+ # self.class.decorator.raise_nil_deserialize_error(%<serialized_form>)
183
+ assert_equal(:raise_nil_deserialize_error, method)
184
+ assert_equal(:str, arg.type)
185
+ elsif method == :default
186
+ # self.class.decorator.props_with_defaults.fetch(%<prop>).default
187
+ assert_equal(:send, receiver.type)
188
+ inner_receiver, inner_method, inner_arg = receiver.children
189
+ assert_equal(
190
+ s(:send, self_class_decorator, :props_with_defaults),
191
+ inner_receiver,
192
+ )
193
+ assert_equal(:fetch, inner_method)
194
+ assert_equal(:sym, inner_arg.type)
195
+ else
196
+ raise ValidationError.new("Unexpected receiver in nil handler: #{node.inspect}")
197
+ end
198
+ else
199
+ raise ValidationError.new("Unexpected nil handler: #{node.inspect}")
200
+ end
201
+ end
202
+
203
+ private_class_method def self.self_class_decorator
204
+ @self_class_decorator ||= s(:send, s(:send, s(:self), :class), :decorator).freeze
205
+ end
206
+
207
+ private_class_method def self.validate_lack_of_side_effects(node, whitelisted_methods_by_receiver_type)
208
+ case node.type
209
+ when :const
210
+ # This is ok, because we'll have validated what method has been called
211
+ # if applicable
212
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :self # rubocop:disable Lint/BooleanSymbol
213
+ # Primitives & self are ok
214
+ when :lvar, :arg, :ivar
215
+ # Reading local & instance variables & arguments is ok
216
+ unless node.children.all? {|c| c.is_a?(Symbol)}
217
+ raise ValidationError.new("Unexpected child for #{node.type}: #{node.inspect}")
218
+ end
219
+ when :args, :mlhs, :block, :begin, :if
220
+ # Blocks etc are read-only if their contents are read-only
221
+ node.children.each {|c| validate_lack_of_side_effects(c, whitelisted_methods_by_receiver_type) if c}
222
+ when :send
223
+ # Sends are riskier so check a whitelist
224
+ receiver, method, *args = node.children
225
+ if receiver
226
+ if receiver.type == :send
227
+ key = receiver
228
+ else
229
+ key = receiver.type
230
+ validate_lack_of_side_effects(receiver, whitelisted_methods_by_receiver_type)
231
+ end
232
+
233
+ if !whitelisted_methods_by_receiver_type[key]&.include?(method)
234
+ raise ValidationError.new("Unexpected method #{method} called on #{receiver.inspect}")
235
+ end
236
+ end
237
+ args.each do |arg|
238
+ validate_lack_of_side_effects(arg, whitelisted_methods_by_receiver_type)
239
+ end
240
+ else
241
+ raise ValidationError.new("Unexpected node type #{node.type}: #{node.inspect}")
242
+ end
243
+ end
244
+
245
+ private_class_method def self.assert_equal(expected, actual)
246
+ if expected != actual
247
+ raise ValidationError.new("Expected #{expected}, got #{actual}")
248
+ end
249
+ end
250
+
251
+ # Method calls generated by SerdeTransform
252
+ private_class_method def self.whitelisted_methods_for_serialize
253
+ @whitelisted_methods_for_serialize ||= {
254
+ lvar: %i{dup map transform_values transform_keys each_with_object nil? []= serialize},
255
+ ivar: %i[dup map transform_values transform_keys each_with_object serialize],
256
+ const: %i[checked_serialize deep_clone_object],
257
+ }
258
+ end
259
+
260
+ # Method calls generated by SerdeTransform
261
+ private_class_method def self.whitelisted_methods_for_deserialize
262
+ @whitelisted_methods_for_deserialize ||= {
263
+ lvar: %i{dup map transform_values transform_keys each_with_object nil? []= to_f},
264
+ const: %i[deserialize from_hash deep_clone_object soft_assert_handler],
265
+ }
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,92 @@
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
+ #
31
+ # Note it does _not_ prevent explicit calls to `eagerly_define_lazy_methods!`
32
+ # from working.
33
+ sig {void}
34
+ def self.disable_lazy_evaluation!
35
+ @lazy_evaluation_disabled ||= true
36
+ end
37
+
38
+ sig {returns(T::Boolean)}
39
+ def self.lazy_evaluation_enabled?
40
+ !@lazy_evaluation_disabled
41
+ end
42
+
43
+ module DecoratorMethods
44
+ extend T::Sig
45
+
46
+ sig {returns(T::Hash[Symbol, T.proc.returns(String)]).checked(:never)}
47
+ private def lazily_defined_methods
48
+ @lazily_defined_methods ||= {}
49
+ end
50
+
51
+ sig {params(name: Symbol).void}
52
+ private def eval_lazily_defined_method!(name)
53
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
54
+ raise SourceEvaluationDisabled.new
55
+ end
56
+
57
+ source = lazily_defined_methods.fetch(name).call
58
+
59
+ cls = decorated_class
60
+ cls.class_eval(source.to_s)
61
+ cls.send(:private, name)
62
+ end
63
+
64
+ sig {params(name: Symbol, blk: T.proc.returns(String)).void}
65
+ private def enqueue_lazy_method_definition!(name, &blk)
66
+ lazily_defined_methods[name] = blk
67
+
68
+ cls = decorated_class
69
+ cls.send(:define_method, name) do |*args|
70
+ self.class.decorator.send(:eval_lazily_defined_method!, name)
71
+ send(name, *args)
72
+ end
73
+ if cls.respond_to?(:ruby2_keywords, true)
74
+ cls.send(:ruby2_keywords, name)
75
+ end
76
+ cls.send(:private, name)
77
+ end
78
+
79
+ sig {void}
80
+ def eagerly_define_lazy_methods!
81
+ return if lazily_defined_methods.empty?
82
+
83
+ source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
84
+
85
+ cls = decorated_class
86
+ cls.class_eval(source)
87
+ lazily_defined_methods.each_key {|name| cls.send(:private, name)}
88
+ lazily_defined_methods.clear
89
+ end
90
+ end
91
+ end
92
+ end
@@ -12,51 +12,58 @@ end
12
12
  # NB: This must stay in the same file where T::Props::Optional is defined due to
13
13
  # 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
14
14
  module T::Props::Optional::DecoratorMethods
15
- # TODO: clean this up. This set of options is confusing, and some of them are not universally
16
- # applicable (e.g., :on_load only applies when using T::Serializable).
17
- VALID_OPTIONAL_RULES = Set[
18
- :existing, # deprecated
19
- :on_load,
20
- false,
21
- true,
22
- ].freeze
23
-
24
- def valid_props
25
- super + [
26
- :default,
27
- :factory,
28
- :optional,
29
- ]
30
- end
15
+ extend T::Sig
31
16
 
32
- def prop_optional?(prop); prop_rules(prop)[:fully_optional]; end
17
+ VALID_RULE_KEYS = {
18
+ default: true,
19
+ factory: true,
20
+ }.freeze
21
+ private_constant :VALID_RULE_KEYS
33
22
 
34
- def mutate_prop_backdoor!(prop, key, value)
35
- rules = props.fetch(prop)
36
- rules = rules.merge(key => value)
37
- compute_derived_rules(rules)
38
- @props = props.merge(prop => rules.freeze).freeze
23
+ DEFAULT_SETTER_RULE_KEY = :_t_props_private_apply_default
24
+ private_constant :DEFAULT_SETTER_RULE_KEY
25
+
26
+ def valid_rule_key?(key)
27
+ super || VALID_RULE_KEYS[key]
39
28
  end
40
29
 
30
+ def prop_optional?(prop); prop_rules(prop)[:fully_optional]; end
31
+
41
32
  def compute_derived_rules(rules)
42
33
  rules[:fully_optional] = !T::Props::Utils.need_nil_write_check?(rules)
43
34
  rules[:need_nil_read_check] = T::Props::Utils.need_nil_read_check?(rules)
44
35
  end
45
36
 
37
+ # checked(:never) - O(runtime object construction)
38
+ sig {returns(T::Hash[Symbol, T::Props::Private::ApplyDefault]).checked(:never)}
39
+ attr_reader :props_with_defaults
40
+
41
+ # checked(:never) - O(runtime object construction)
42
+ sig {returns(T::Hash[Symbol, T::Props::Private::SetterFactory::SetterProc]).checked(:never)}
43
+ attr_reader :props_without_defaults
44
+
46
45
  def add_prop_definition(prop, rules)
47
46
  compute_derived_rules(rules)
47
+
48
+ default_setter = T::Props::Private::ApplyDefault.for(decorated_class, rules)
49
+ if default_setter
50
+ @props_with_defaults ||= {}
51
+ @props_with_defaults[prop] = default_setter
52
+ @props_without_defaults&.delete(prop) # Handle potential override
53
+
54
+ rules[DEFAULT_SETTER_RULE_KEY] = default_setter
55
+ else
56
+ @props_without_defaults ||= {}
57
+ @props_without_defaults[prop] = rules.fetch(:setter_proc)
58
+ @props_with_defaults&.delete(prop) # Handle potential override
59
+ end
60
+
48
61
  super
49
62
  end
50
63
 
51
64
  def prop_validate_definition!(name, cls, rules, type)
52
65
  result = super
53
66
 
54
- if (rules_optional = rules[:optional])
55
- if !VALID_OPTIONAL_RULES.include?(rules_optional)
56
- raise ArgumentError.new(":optional must be one of #{VALID_OPTIONAL_RULES.inspect}")
57
- end
58
- end
59
-
60
67
  if rules.key?(:default) && rules.key?(:factory)
61
68
  raise ArgumentError.new("Setting both :default and :factory is invalid. See: go/chalk-docs")
62
69
  end
@@ -65,21 +72,10 @@ module T::Props::Optional::DecoratorMethods
65
72
  end
66
73
 
67
74
  def has_default?(rules)
68
- rules.include?(:default) || rules.include?(:factory)
75
+ rules.include?(DEFAULT_SETTER_RULE_KEY)
69
76
  end
70
77
 
71
78
  def get_default(rules, instance_class)
72
- if rules.include?(:default)
73
- default = rules[:default]
74
- T::Props::Utils.deep_clone_object(default)
75
- elsif rules.include?(:factory)
76
- # Factory should never be nil if the key is specified, but
77
- # we do this rather than 'elsif rules[:factory]' for
78
- # consistency with :default.
79
- factory = rules[:factory]
80
- instance_class.class_exec(&factory)
81
- else
82
- nil
83
- end
79
+ rules[DEFAULT_SETTER_RULE_KEY]&.default
84
80
  end
85
81
  end