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.
- checksums.yaml +4 -4
- data/lib/sorbet-runtime.rb +10 -1
- data/lib/types/_types.rb +10 -9
- data/lib/types/compatibility_patches.rb +65 -8
- data/lib/types/configuration.rb +3 -3
- data/lib/types/enum.rb +33 -16
- data/lib/types/generic.rb +2 -2
- data/lib/types/interface_wrapper.rb +4 -4
- data/lib/types/non_forcing_constants.rb +57 -0
- data/lib/types/private/abstract/data.rb +2 -2
- data/lib/types/private/abstract/declare.rb +3 -0
- data/lib/types/private/casts.rb +27 -0
- data/lib/types/private/class_utils.rb +8 -5
- data/lib/types/private/methods/_methods.rb +73 -23
- data/lib/types/private/methods/call_validation.rb +3 -0
- data/lib/types/private/methods/signature.rb +16 -6
- data/lib/types/private/retry.rb +10 -0
- data/lib/types/private/sealed.rb +1 -1
- data/lib/types/private/types/type_alias.rb +5 -0
- data/lib/types/private/types/void.rb +4 -3
- data/lib/types/profile.rb +5 -1
- data/lib/types/props/_props.rb +1 -5
- data/lib/types/props/constructor.rb +29 -9
- data/lib/types/props/custom_type.rb +51 -27
- data/lib/types/props/decorator.rb +70 -207
- data/lib/types/props/generated_code_validation.rb +268 -0
- data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
- data/lib/types/props/optional.rb +28 -28
- data/lib/types/props/plugin.rb +2 -2
- data/lib/types/props/private/apply_default.rb +170 -0
- data/lib/types/props/private/deserializer_generator.rb +165 -0
- data/lib/types/props/private/parser.rb +32 -0
- data/lib/types/props/private/serde_transform.rb +192 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/private/setter_factory.rb +78 -26
- data/lib/types/props/serializable.rb +126 -181
- data/lib/types/props/type_validation.rb +10 -1
- data/lib/types/props/weak_constructor.rb +51 -14
- data/lib/types/sig.rb +3 -3
- data/lib/types/types/attached_class.rb +5 -1
- data/lib/types/types/base.rb +13 -0
- data/lib/types/types/fixed_array.rb +28 -2
- data/lib/types/types/fixed_hash.rb +10 -9
- data/lib/types/types/intersection.rb +5 -0
- data/lib/types/types/noreturn.rb +4 -0
- data/lib/types/types/self_type.rb +4 -0
- data/lib/types/types/simple.rb +22 -1
- data/lib/types/types/t_enum.rb +6 -1
- data/lib/types/types/type_parameter.rb +1 -1
- data/lib/types/types/typed_array.rb +7 -2
- data/lib/types/types/typed_enumerable.rb +23 -7
- data/lib/types/types/typed_enumerator.rb +7 -2
- data/lib/types/types/typed_hash.rb +8 -3
- data/lib/types/types/typed_range.rb +7 -2
- data/lib/types/types/typed_set.rb +7 -2
- data/lib/types/types/union.rb +36 -5
- data/lib/types/types/untyped.rb +4 -0
- data/lib/types/utils.rb +21 -6
- 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
|
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
|
-
|
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=
|
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?(
|
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
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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:
|
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,
|
399
|
+
private def smart_coerce(type, enum:)
|
549
400
|
# Backwards compatibility for pre-T::Types style
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
T
|
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
|
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:
|
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
|