-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
type_validation.rb
114 lines (104 loc) 路 3.66 KB
/
type_validation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# frozen_string_literal: true
# typed: false
module T::Props::TypeValidation
include T::Props::Plugin
BANNED_TYPES = [Object, BasicObject, Kernel].freeze
class UnderspecifiedType < ArgumentError; end
module DecoratorMethods
extend T::Sig
sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
def valid_rule_key?(key)
super || key == :DEPRECATED_underspecified_type
end
# checked(:never) - Rules hash is expensive to check
sig do
params(
name: T.any(Symbol, String),
_cls: Module,
rules: T::Hash[Symbol, T.untyped],
type: T.any(T::Types::Base, Module)
)
.void
.checked(:never)
end
def prop_validate_definition!(name, _cls, rules, type)
super
if !rules[:DEPRECATED_underspecified_type]
validate_type(type, name)
elsif rules[:DEPRECATED_underspecified_type] && find_invalid_subtype(type).nil?
raise ArgumentError.new("DEPRECATED_underspecified_type set unnecessarily for #{@class.name}.#{name} - #{type} is a valid type")
end
end
sig do
params(
type: T::Types::Base,
field_name: T.any(Symbol, String),
)
.void
.checked(:never)
end
private def validate_type(type, field_name)
if (invalid_subtype = find_invalid_subtype(type))
raise UnderspecifiedType.new(type_error_message(invalid_subtype, field_name, type))
end
end
# Returns an invalid type, if any, found in the given top-level type.
# This might be the type itself, if it is e.g. "Object", or might be
# a subtype like the type of the values of a typed hash.
#
# If the type is fully valid, returns nil.
#
# checked(:never) - called potentially many times recursively
sig {params(type: T::Types::Base).returns(T.nilable(T::Types::Base)).checked(:never)}
private def find_invalid_subtype(type)
case type
when T::Types::TypedEnumerable
find_invalid_subtype(type.type)
when T::Types::FixedHash
type.types.values.map {|subtype| find_invalid_subtype(subtype)}.compact.first
when T::Types::Union, T::Types::FixedArray
# `T.any` is valid if all of the members are valid
type.types.map {|subtype| find_invalid_subtype(subtype)}.compact.first
when T::Types::Intersection
# `T.all` is valid if at least one of the members is valid
invalid = type.types.map {|subtype| find_invalid_subtype(subtype)}.compact
if invalid.length == type.types.length
invalid.first
else
nil
end
when T::Types::Enum, T::Types::ClassOf
nil
when T::Private::Types::TypeAlias
find_invalid_subtype(type.aliased_type)
when T::Types::Simple
# TODO Could we manage to define a whitelist, consisting of something
# like primitives, subdocs, DataInterfaces, and collections/enums/unions
# thereof?
if BANNED_TYPES.include?(type.raw_type)
type
else
nil
end
else
type
end
end
sig do
params(
type: T::Types::Base,
field_name: T.any(Symbol, String),
orig_type: T::Types::Base,
)
.returns(String)
end
private def type_error_message(type, field_name, orig_type)
msg_prefix = "#{@class.name}.#{field_name}: #{orig_type} is invalid in prop definition"
if type == orig_type
"#{msg_prefix}. Please choose a more specific type (T.untyped and ~equivalents like Object are banned)."
else
"#{msg_prefix}. Please choose a subtype more specific than #{type} (T.untyped and ~equivalents like Object are banned)."
end
end
end
end