sorbet-runtime 0.5.5316 → 0.5.5956

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sorbet-runtime.rb +10 -1
  3. data/lib/types/_types.rb +10 -9
  4. data/lib/types/compatibility_patches.rb +65 -8
  5. data/lib/types/configuration.rb +3 -3
  6. data/lib/types/enum.rb +33 -16
  7. data/lib/types/generic.rb +2 -2
  8. data/lib/types/interface_wrapper.rb +4 -4
  9. data/lib/types/non_forcing_constants.rb +57 -0
  10. data/lib/types/private/abstract/data.rb +2 -2
  11. data/lib/types/private/abstract/declare.rb +3 -0
  12. data/lib/types/private/casts.rb +27 -0
  13. data/lib/types/private/class_utils.rb +8 -5
  14. data/lib/types/private/methods/_methods.rb +73 -23
  15. data/lib/types/private/methods/call_validation.rb +3 -0
  16. data/lib/types/private/methods/signature.rb +16 -6
  17. data/lib/types/private/retry.rb +10 -0
  18. data/lib/types/private/sealed.rb +1 -1
  19. data/lib/types/private/types/type_alias.rb +5 -0
  20. data/lib/types/private/types/void.rb +4 -3
  21. data/lib/types/profile.rb +5 -1
  22. data/lib/types/props/_props.rb +1 -5
  23. data/lib/types/props/constructor.rb +29 -9
  24. data/lib/types/props/custom_type.rb +51 -27
  25. data/lib/types/props/decorator.rb +70 -207
  26. data/lib/types/props/generated_code_validation.rb +268 -0
  27. data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
  28. data/lib/types/props/optional.rb +28 -28
  29. data/lib/types/props/plugin.rb +2 -2
  30. data/lib/types/props/private/apply_default.rb +170 -0
  31. data/lib/types/props/private/deserializer_generator.rb +165 -0
  32. data/lib/types/props/private/parser.rb +32 -0
  33. data/lib/types/props/private/serde_transform.rb +192 -0
  34. data/lib/types/props/private/serializer_generator.rb +77 -0
  35. data/lib/types/props/private/setter_factory.rb +78 -26
  36. data/lib/types/props/serializable.rb +126 -181
  37. data/lib/types/props/type_validation.rb +10 -1
  38. data/lib/types/props/weak_constructor.rb +51 -14
  39. data/lib/types/sig.rb +3 -3
  40. data/lib/types/types/attached_class.rb +5 -1
  41. data/lib/types/types/base.rb +13 -0
  42. data/lib/types/types/fixed_array.rb +28 -2
  43. data/lib/types/types/fixed_hash.rb +10 -9
  44. data/lib/types/types/intersection.rb +5 -0
  45. data/lib/types/types/noreturn.rb +4 -0
  46. data/lib/types/types/self_type.rb +4 -0
  47. data/lib/types/types/simple.rb +22 -1
  48. data/lib/types/types/t_enum.rb +6 -1
  49. data/lib/types/types/type_parameter.rb +1 -1
  50. data/lib/types/types/typed_array.rb +7 -2
  51. data/lib/types/types/typed_enumerable.rb +23 -7
  52. data/lib/types/types/typed_enumerator.rb +7 -2
  53. data/lib/types/types/typed_hash.rb +8 -3
  54. data/lib/types/types/typed_range.rb +7 -2
  55. data/lib/types/types/typed_set.rb +7 -2
  56. data/lib/types/types/union.rb +36 -5
  57. data/lib/types/types/untyped.rb +4 -0
  58. data/lib/types/utils.rb +21 -6
  59. metadata +95 -2
@@ -12,7 +12,7 @@ class T::Props::Decorator
12
12
 
13
13
  Rules = T.type_alias {T::Hash[Symbol, T.untyped]}
14
14
  DecoratedInstance = T.type_alias {Object} # Would be T::Props, but that produces circular reference errors in some circumstances
15
- PropType = T.type_alias {T.any(T::Types::Base, T::Props::CustomType)}
15
+ PropType = T.type_alias {T::Types::Base}
16
16
  PropTypeOrClass = T.type_alias {T.any(PropType, Module)}
17
17
 
18
18
  class NoRulesError < StandardError; end
@@ -20,7 +20,7 @@ class T::Props::Decorator
20
20
  EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
21
21
  private_constant :EMPTY_PROPS
22
22
 
23
- sig {params(klass: T.untyped).void}
23
+ sig {params(klass: T.untyped).void.checked(:never)}
24
24
  def initialize(klass)
25
25
  @class = T.let(klass, T.all(Module, T::Props::ClassMethods))
26
26
  @class.plugins.each do |mod|
@@ -66,7 +66,7 @@ class T::Props::Decorator
66
66
  without_accessors
67
67
  clobber_existing_method!
68
68
  extra
69
- optional
69
+ setter_validate
70
70
  _tnilable
71
71
  }.map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
72
72
  private_constant :VALID_RULE_KEYS
@@ -118,6 +118,21 @@ class T::Props::Decorator
118
118
  end
119
119
  alias_method :set, :prop_set
120
120
 
121
+ # Only Models have any custom get logic but we need to call this on
122
+ # non-Models since we don't know at code gen time what we have.
123
+ sig do
124
+ params(
125
+ instance: DecoratedInstance,
126
+ prop: Symbol,
127
+ value: T.untyped
128
+ )
129
+ .returns(T.untyped)
130
+ .checked(:never)
131
+ end
132
+ def prop_get_logic(instance, prop, value)
133
+ value
134
+ end
135
+
121
136
  # For performance, don't use named params here.
122
137
  # Passing in rules here is purely a performance optimization.
123
138
  #
@@ -173,7 +188,7 @@ class T::Props::Decorator
173
188
  .returns(T.untyped)
174
189
  .checked(:never)
175
190
  end
176
- def foreign_prop_get(instance, prop, foreign_class, rules=props[prop.to_sym], opts={})
191
+ def foreign_prop_get(instance, prop, foreign_class, rules=prop_rules(prop), opts={})
177
192
  return if !(value = prop_get(instance, prop, rules))
178
193
  T.unsafe(foreign_class).load(value, {}, opts)
179
194
  end
@@ -204,12 +219,6 @@ class T::Props::Decorator
204
219
  raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
205
220
  end
206
221
 
207
- if (array = rules[:array])
208
- unless array.is_a?(Module)
209
- raise ArgumentError.new("Bad class as subtype in prop #{@class.name}.#{name}: #{array.inspect}")
210
- end
211
- end
212
-
213
222
  if !(rules[:clobber_existing_method!]) && !(rules[:without_accessors])
214
223
  if BANNED_METHOD_NAMES.include?(name.to_sym)
215
224
  raise ArgumentError.new(
@@ -229,10 +238,12 @@ class T::Props::Decorator
229
238
  nil
230
239
  end
231
240
 
241
+ SAFE_NAME = /\A[A-Za-z_][A-Za-z0-9_-]*\z/
242
+
232
243
  # Used to validate both prop names and serialized forms
233
244
  sig {params(name: T.any(Symbol, String)).void}
234
245
  private def validate_prop_name(name)
235
- if !name.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/)
246
+ if !name.match?(SAFE_NAME)
236
247
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
237
248
  end
238
249
  end
@@ -277,47 +288,6 @@ class T::Props::Decorator
277
288
  end
278
289
  def prop_defined(name, cls, rules={})
279
290
  cls = T::Utils.resolve_alias(cls)
280
- if rules[:optional] == true
281
- T::Configuration.hard_assert_handler(
282
- 'Use of `optional: true` is deprecated, please use `T.nilable(...)` instead.',
283
- storytime: {
284
- name: name,
285
- cls_or_args: cls.to_s,
286
- args: rules,
287
- klass: decorated_class.name,
288
- },
289
- )
290
- elsif rules[:optional] == false
291
- T::Configuration.hard_assert_handler(
292
- 'Use of `optional: :false` is deprecated as it\'s the default value.',
293
- storytime: {
294
- name: name,
295
- cls_or_args: cls.to_s,
296
- args: rules,
297
- klass: decorated_class.name,
298
- },
299
- )
300
- elsif rules[:optional] == :on_load
301
- T::Configuration.hard_assert_handler(
302
- 'Use of `optional: :on_load` is deprecated. You probably want `T.nilable(...)` with :raise_on_nil_write instead.',
303
- storytime: {
304
- name: name,
305
- cls_or_args: cls.to_s,
306
- args: rules,
307
- klass: decorated_class.name,
308
- },
309
- )
310
- elsif rules[:optional] == :existing
311
- T::Configuration.hard_assert_handler(
312
- 'Use of `optional: :existing` is not allowed: you should use use T.nilable (http://go/optional)',
313
- storytime: {
314
- name: name,
315
- cls_or_args: cls.to_s,
316
- args: rules,
317
- klass: decorated_class.name,
318
- },
319
- )
320
- end
321
291
 
322
292
  if T::Utils::Nilable.is_union_with_nilclass(cls)
323
293
  # :_tnilable is introduced internally for performance purpose so that clients do not need to call
@@ -332,21 +302,13 @@ class T::Props::Decorator
332
302
  if !cls.is_a?(Module)
333
303
  cls = convert_type_to_class(cls)
334
304
  end
335
- type_object = type
336
- if !(type_object.singleton_class < T::Props::CustomType)
337
- type_object = smart_coerce(type_object, array: rules[:array], enum: rules[:enum])
338
- end
305
+ type_object = smart_coerce(type, enum: rules[:enum])
339
306
 
340
307
  prop_validate_definition!(name, cls, rules, type_object)
341
308
 
342
309
  # Retrive the possible underlying object with T.nilable.
343
- underlying_type_object = T::Utils::Nilable.get_underlying_type_object(type_object)
344
310
  type = T::Utils::Nilable.get_underlying_type(type)
345
311
 
346
- array_subdoc_type = array_subdoc_type(underlying_type_object)
347
- hash_value_subdoc_type = hash_value_subdoc_type(underlying_type_object)
348
- hash_key_custom_type = hash_key_custom_type(underlying_type_object)
349
-
350
312
  sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
351
313
  if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::Utils)
352
314
  sensitivity_and_pii = Opus::Sensitivity::Utils.normalize_sensitivity_and_pii_annotation(sensitivity_and_pii)
@@ -364,27 +326,11 @@ class T::Props::Decorator
364
326
  end
365
327
  end
366
328
 
367
- needs_clone =
368
- if cls <= Array || cls <= Hash || cls <= Set
369
- shallow_clone_ok(underlying_type_object) ? :shallow : true
370
- else
371
- false
372
- end
373
-
374
329
  rules = rules.merge(
375
330
  # TODO: The type of this element is confusing. We should refactor so that
376
331
  # it can be always `type_object` (a PropType) or always `cls` (a Module)
377
332
  type: type,
378
- # These are precomputed for performance
379
- # TODO: A lot of these are only needed by T::Props::Serializable or T::Struct
380
- # and can/should be moved accordingly.
381
- type_is_custom_type: cls.singleton_class < T::Props::CustomType,
382
- type_is_serializable: cls < T::Props::Serializable,
383
- type_is_array_of_serializable: !array_subdoc_type.nil?,
384
- type_is_hash_of_serializable_values: !hash_value_subdoc_type.nil?,
385
- type_is_hash_of_custom_type_keys: !hash_key_custom_type.nil?,
386
333
  type_object: type_object,
387
- type_needs_clone: needs_clone,
388
334
  accessor_key: "@#{name}".to_sym,
389
335
  sensitivity: sensitivity_and_pii[:sensitivity],
390
336
  pii: sensitivity_and_pii[:pii],
@@ -394,26 +340,13 @@ class T::Props::Decorator
394
340
 
395
341
  validate_not_missing_sensitivity(name, rules)
396
342
 
397
- # for backcompat
398
- if type.is_a?(T::Types::TypedArray) && type.type.is_a?(T::Types::Simple)
399
- rules[:array] = type.type.raw_type
400
- elsif array_subdoc_type
401
- rules[:array] = array_subdoc_type
402
- end
403
-
404
- if rules[:type_is_serializable]
405
- rules[:serializable_subtype] = cls
406
- elsif array_subdoc_type
407
- rules[:serializable_subtype] = array_subdoc_type
408
- elsif hash_value_subdoc_type && hash_key_custom_type
409
- rules[:serializable_subtype] = {
410
- keys: hash_key_custom_type,
411
- values: hash_value_subdoc_type,
412
- }
413
- elsif hash_value_subdoc_type
414
- rules[:serializable_subtype] = hash_value_subdoc_type
415
- elsif hash_key_custom_type
416
- rules[:serializable_subtype] = hash_key_custom_type
343
+ # for backcompat (the `:array` key is deprecated but because the name is
344
+ # so generic it's really hard to be sure it's not being relied on anymore)
345
+ if type.is_a?(T::Types::TypedArray)
346
+ inner = T::Utils::Nilable.get_underlying_type(type.type)
347
+ if inner.is_a?(Module)
348
+ rules[:array] = inner
349
+ end
417
350
  end
418
351
 
419
352
  rules[:setter_proc] = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules).freeze
@@ -429,7 +362,7 @@ class T::Props::Decorator
429
362
  end
430
363
 
431
364
  handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
432
- handle_foreign_hint_only_option(cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
365
+ handle_foreign_hint_only_option(name, cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
433
366
  handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
434
367
  end
435
368
 
@@ -459,110 +392,22 @@ class T::Props::Decorator
459
392
  end
460
393
  end
461
394
 
462
- # returns the subdoc of the array type, or nil if it's not a Document type
463
- #
464
- # checked(:never) - Typechecks internally
465
- sig do
466
- params(type: PropType)
467
- .returns(T.nilable(Module))
468
- .checked(:never)
469
- end
470
- private def array_subdoc_type(type)
471
- if type.is_a?(T::Types::TypedArray)
472
- el_type = T::Utils.unwrap_nilable(type.type) || type.type
473
-
474
- if el_type.is_a?(T::Types::Simple) &&
475
- (el_type.raw_type < T::Props::Serializable || el_type.raw_type.is_a?(T::Props::CustomType))
476
- return el_type.raw_type
477
- end
478
- end
479
-
480
- nil
481
- end
482
-
483
- # returns the subdoc of the hash value type, or nil if it's not a Document type
484
- #
485
- # checked(:never) - Typechecks internally
486
- sig do
487
- params(type: PropType)
488
- .returns(T.nilable(Module))
489
- .checked(:never)
490
- end
491
- private def hash_value_subdoc_type(type)
492
- if type.is_a?(T::Types::TypedHash)
493
- values_type = T::Utils.unwrap_nilable(type.values) || type.values
494
-
495
- if values_type.is_a?(T::Types::Simple) &&
496
- (values_type.raw_type < T::Props::Serializable || values_type.raw_type.is_a?(T::Props::CustomType))
497
- return values_type.raw_type
498
- end
499
- end
500
-
501
- nil
502
- end
503
-
504
- # returns the type of the hash key, or nil. Any CustomType could be a key, but we only expect T::Enum right now.
505
- #
506
- # checked(:never) - Typechecks internally
507
395
  sig do
508
- params(type: PropType)
509
- .returns(T.nilable(Module))
510
- .checked(:never)
511
- end
512
- private def hash_key_custom_type(type)
513
- if type.is_a?(T::Types::TypedHash)
514
- keys_type = T::Utils.unwrap_nilable(type.keys) || type.keys
515
-
516
- if keys_type.is_a?(T::Types::Simple) && keys_type.raw_type.is_a?(T::Props::CustomType)
517
- return keys_type.raw_type
518
- end
519
- end
520
-
521
- nil
522
- end
523
-
524
- # From T::Props::Utils.deep_clone_object, plus String
525
- TYPES_NOT_NEEDING_CLONE = T.let([TrueClass, FalseClass, NilClass, Symbol, String, Numeric], T::Array[Module])
526
-
527
- # checked(:never) - Typechecks internally
528
- sig {params(type: PropType).returns(T.nilable(T::Boolean)).checked(:never)}
529
- private def shallow_clone_ok(type)
530
- inner_type =
531
- if type.is_a?(T::Types::TypedArray)
532
- type.type
533
- elsif type.is_a?(T::Types::TypedSet)
534
- type.type
535
- elsif type.is_a?(T::Types::TypedHash)
536
- type.values
537
- end
538
-
539
- inner_type.is_a?(T::Types::Simple) && TYPES_NOT_NEEDING_CLONE.any? do |cls|
540
- inner_type.raw_type <= cls
541
- end
542
- end
543
-
544
- sig do
545
- params(type: PropTypeOrClass, array: T.untyped, enum: T.untyped)
396
+ params(type: PropTypeOrClass, enum: T.untyped)
546
397
  .returns(T::Types::Base)
547
398
  end
548
- private def smart_coerce(type, array:, enum:)
399
+ private def smart_coerce(type, enum:)
549
400
  # Backwards compatibility for pre-T::Types style
550
- if !array.nil? && !enum.nil?
551
- raise ArgumentError.new("Cannot specify both :array and :enum options")
552
- elsif !array.nil?
553
- if type == Set
554
- T::Set[array]
555
- else
556
- T::Array[array]
557
- end
558
- elsif !enum.nil?
559
- if T::Utils.unwrap_nilable(type)
560
- T.nilable(T.enum(enum))
401
+ type = T::Utils.coerce(type)
402
+ if enum.nil?
403
+ type
404
+ else
405
+ nonnil_type = T::Utils.unwrap_nilable(type)
406
+ if nonnil_type
407
+ T.nilable(T.all(nonnil_type, T.enum(enum)))
561
408
  else
562
- T.enum(enum)
409
+ T.all(type, T.enum(enum))
563
410
  end
564
- else
565
- T::Utils.coerce(type)
566
411
  end
567
412
  end
568
413
 
@@ -582,7 +427,7 @@ class T::Props::Decorator
582
427
  # TODO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
583
428
  # other terms, but this interacts badly with ProtoDefinedDocument because
584
429
  # the proto syntax currently can't declare "sensitivity: []"
585
- if prop_name =~ /\bsecret\b/
430
+ if /\bsecret\b/.match?(prop_name)
586
431
  T::Configuration.hard_assert_handler(
587
432
  "#{@class}##{prop_name} has the word 'secret' in its name, but no " \
588
433
  "'sensitivity:' annotation. This is probably wrong, because if a " \
@@ -599,7 +444,7 @@ class T::Props::Decorator
599
444
  sig do
600
445
  params(
601
446
  prop_name: Symbol,
602
- redaction: Chalk::Tools::RedactionUtils::RedactionDirectiveSpec,
447
+ redaction: T.untyped,
603
448
  )
604
449
  .void
605
450
  end
@@ -636,12 +481,13 @@ class T::Props::Decorator
636
481
 
637
482
  sig do
638
483
  params(
484
+ prop_name: Symbol,
639
485
  prop_cls: Module,
640
486
  foreign_hint_only: T.untyped,
641
487
  )
642
488
  .void
643
489
  end
644
- private def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)
490
+ private def handle_foreign_hint_only_option(prop_name, prop_cls, foreign_hint_only)
645
491
  if ![String, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
646
492
  raise ArgumentError.new(
647
493
  "`foreign_hint_only` can only be used with String or Array prop types"
@@ -652,6 +498,21 @@ class T::Props::Decorator
652
498
  :foreign_hint_only, foreign_hint_only,
653
499
  valid_type_msg: "an individual or array of a model class, or a Proc returning such."
654
500
  )
501
+
502
+ unless foreign_hint_only.is_a?(Proc)
503
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign_hint_only}, notify: 'jerry')
504
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign_hint_only`. In other words:
505
+
506
+ instead of `prop :foo, String, foreign_hint_only: FooModel`
507
+ use `prop :foo, String, foreign_hint_only: -> {FooModel}`
508
+
509
+ OR
510
+
511
+ instead of `prop :foo, String, foreign_hint_only: [FooModel, BarModel]`
512
+ use `prop :foo, String, foreign_hint_only: -> {[FooModel, BarModel]}`
513
+
514
+ MESSAGE
515
+ end
655
516
  end
656
517
 
657
518
  # checked(:never) - Rules hash is expensive to check
@@ -705,14 +566,6 @@ class T::Props::Decorator
705
566
  end
706
567
  loaded_foreign
707
568
  end
708
-
709
- @class.send(:define_method, "#{prop_name}_record") do |allow_direct_mutation: nil|
710
- T::Configuration.soft_assert_handler(
711
- "Using deprecated 'model.#{prop_name}_record' foreign key syntax. You should replace this with 'model.#{prop_name}_'",
712
- notify: 'vasi'
713
- )
714
- send(fk_method, allow_direct_mutation: allow_direct_mutation)
715
- end
716
569
  end
717
570
 
718
571
  # checked(:never) - Rules hash is expensive to check
@@ -745,6 +598,16 @@ class T::Props::Decorator
745
598
  )
746
599
  end
747
600
 
601
+ unless foreign.is_a?(Proc)
602
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign}, notify: 'jerry')
603
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign`. In other words:
604
+
605
+ instead of `prop :foo, String, foreign: FooModel`
606
+ use `prop :foo, String, foreign: -> {FooModel}`
607
+
608
+ MESSAGE
609
+ end
610
+
748
611
  define_foreign_method(prop_name, rules, foreign)
749
612
  end
750
613
 
@@ -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 % 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
+
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