typeguard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.rubocop.yml +25 -0
- data/.rubocop_todo.yml +89 -0
- data/Gemfile +16 -0
- data/LICENSE +21 -0
- data/README.md +42 -0
- data/Rakefile +12 -0
- data/lib/typeguard/configuration.rb +57 -0
- data/lib/typeguard/metrics.rb +73 -0
- data/lib/typeguard/resolver.rb +88 -0
- data/lib/typeguard/type_model/builder/rbs_builder.rb +99 -0
- data/lib/typeguard/type_model/builder/yard_builder.rb +237 -0
- data/lib/typeguard/type_model/builder.rb +20 -0
- data/lib/typeguard/type_model/definitions.rb +15 -0
- data/lib/typeguard/type_model/mapper/rbs_mapper.rb +84 -0
- data/lib/typeguard/type_model/mapper/yard_mapper.rb +71 -0
- data/lib/typeguard/types.rb +177 -0
- data/lib/typeguard/validator.rb +83 -0
- data/lib/typeguard/version.rb +5 -0
- data/lib/typeguard/wrapper.rb +101 -0
- data/lib/typeguard.rb +11 -0
- data/sig/typeguard/bin/example.rbs +77 -0
- metadata +118 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
module Typeguard
|
6
|
+
module TypeModel
|
7
|
+
module Builder
|
8
|
+
# Takes YARD documentation and returns a generic type model
|
9
|
+
class YardBuilder
|
10
|
+
include Typeguard::TypeModel::Definitions
|
11
|
+
|
12
|
+
# @see https://rubydoc.info/gems/typeguard/YARD/Registry
|
13
|
+
# @param reparse_files [Boolean] has no effect if target is a string.
|
14
|
+
# If false and target is an array, the files are only reparsed if no .yardoc is present.
|
15
|
+
# If true and target is an array, the files are always reparsed.
|
16
|
+
def initialize(target, reparse_files)
|
17
|
+
@object_vars = {}
|
18
|
+
return unless YARD::Registry.load(target, reparse_files).root.children.empty?
|
19
|
+
|
20
|
+
if target.is_a?(String)
|
21
|
+
puts "WARNING: could not find YARD objects for target directory '#{target}'. " \
|
22
|
+
"Confirm that the directory exists and/or execute 'yardoc [...]' again."
|
23
|
+
else
|
24
|
+
puts "WARNING: could not find YARD objects for target files after reparsing array #{target}. " \
|
25
|
+
"Confirm that the files exist and/or execute 'yardoc #{target.join(' ')}' again in the correct directory."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def build
|
30
|
+
# Deduplicated tree-like structure where the root is an array of
|
31
|
+
# objects whose parent is undefined or YARD root/proxy
|
32
|
+
YARD::Registry.all(:class, :module, :method).filter_map do |object|
|
33
|
+
build_object(object) if object.parent.nil? || %i[root proxy].include?(object.parent.type)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_object(object)
|
38
|
+
case object.type
|
39
|
+
when :class
|
40
|
+
children = object.children.map { |child| build_object(child) }.compact
|
41
|
+
ClassDefinition.new(
|
42
|
+
name: object.path.gsub('.self', ''),
|
43
|
+
source: "#{object.file}:#{object.line}",
|
44
|
+
vars: build_inherit_vars(object),
|
45
|
+
parent: object.superclass&.path,
|
46
|
+
type_parameters: nil,
|
47
|
+
children: children
|
48
|
+
)
|
49
|
+
when :module
|
50
|
+
children = object.children.map { |child| build_object(child) }.compact
|
51
|
+
ModuleDefinition.new(
|
52
|
+
name: object.path.gsub('.self', ''),
|
53
|
+
source: "#{object.file}:#{object.line}",
|
54
|
+
vars: build_inherit_vars(object),
|
55
|
+
type_parameters: nil,
|
56
|
+
children: children
|
57
|
+
)
|
58
|
+
when :method
|
59
|
+
return_tag = object.tag(:return)
|
60
|
+
returns = ReturnDefinition.new(
|
61
|
+
source: "#{object.file}:#{object.line}",
|
62
|
+
types: build_types(return_tag),
|
63
|
+
types_string: build_types_string(return_tag)
|
64
|
+
)
|
65
|
+
MethodDefinition.new(
|
66
|
+
name: object.name,
|
67
|
+
source: "#{object.file}:#{object.line}",
|
68
|
+
scope: object.scope,
|
69
|
+
visibility: object.visibility,
|
70
|
+
parameters: build_parameters(object),
|
71
|
+
returns: returns
|
72
|
+
)
|
73
|
+
when :constant, :classvariable, :proxy
|
74
|
+
# Covered by build_vars and build
|
75
|
+
else
|
76
|
+
raise "Unsupported YARD object: #{object.class}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_parameters(object)
|
81
|
+
unbound_children = object.tags(:option).each_with_object({}) do |tag, hash|
|
82
|
+
index = tag.name.gsub(/:/, '')
|
83
|
+
hash[index] ||= [[], []]
|
84
|
+
key = build_symbol
|
85
|
+
key.metadata[:key] = tag.pair.name.gsub(/:/, '')
|
86
|
+
value = build_types(tag.pair)
|
87
|
+
value.each { |node| node.metadata[:defaults] = tag.pair.defaults }
|
88
|
+
hash[index].first << key
|
89
|
+
hash[index].last << value
|
90
|
+
end
|
91
|
+
|
92
|
+
ps = object.parameters.dup
|
93
|
+
parameters = object.tags(:param).map do |tag|
|
94
|
+
param = ps.find { |name, _| name.gsub(/[*:]/, '') == tag.name }
|
95
|
+
next unless param
|
96
|
+
|
97
|
+
ps.delete(param)
|
98
|
+
bound_children = unbound_children.delete(tag.name)
|
99
|
+
ParameterDefinition.new(
|
100
|
+
name: tag.name.to_sym,
|
101
|
+
source: "#{object.file}:#{object.line}",
|
102
|
+
default: param.last,
|
103
|
+
types: bound_children ? [build_fixed_hash(bound_children)] : build_types(tag),
|
104
|
+
types_string: build_types_string(tag)
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
untyped_defaults = ps.reject { |_, default| default.nil? }
|
109
|
+
untyped_defaults.each do |name, default|
|
110
|
+
parameter = ParameterDefinition.new(
|
111
|
+
name: name.to_sym,
|
112
|
+
source: "#{object.file}:#{object.line}",
|
113
|
+
default: default,
|
114
|
+
types: build_types(nil),
|
115
|
+
types_string: build_types_string(nil)
|
116
|
+
)
|
117
|
+
parameters << parameter
|
118
|
+
end
|
119
|
+
|
120
|
+
unbound_children.each do |k, v|
|
121
|
+
parameter = ParameterDefinition.new(
|
122
|
+
name: k,
|
123
|
+
source: "#{object.file}:#{object.line}",
|
124
|
+
default: nil,
|
125
|
+
types: [build_fixed_hash(v)],
|
126
|
+
types_string: 'Hash'
|
127
|
+
)
|
128
|
+
parameters << parameter
|
129
|
+
end
|
130
|
+
|
131
|
+
parameters
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_inherit_vars(object)
|
135
|
+
# Looks at mixins and superclasses to build a full set
|
136
|
+
# of (inherited) vars, order-preserved such that the
|
137
|
+
# narrowest namespace takes precedence: if class A and
|
138
|
+
# B < A define attribute c, the definition of A::B holds
|
139
|
+
# pp "build_inherit_vars for #{object}"
|
140
|
+
# object.inheritance_tree(true).flat_map do |inherited|
|
141
|
+
object.inheritance_tree(true).flat_map do |inherited|
|
142
|
+
@object_vars[inherited] ||= build_vars(inherited)
|
143
|
+
end.uniq(&:name)
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_vars(object)
|
147
|
+
return [] unless %i[class module].include?(object.type)
|
148
|
+
|
149
|
+
# NOTE: When a module is defined with .self syntax
|
150
|
+
# and also referenced with :: syntax, the reference
|
151
|
+
# is interpreted as a proxy. You could eventually
|
152
|
+
# find the actual code object, but iteratively
|
153
|
+
# replacing every :: with .self or vice versa and
|
154
|
+
# performing the lookup is not very nice. So, we
|
155
|
+
# simply don't propagate in this case.
|
156
|
+
return [] if object.is_a? YARD::CodeObjects::Proxy
|
157
|
+
|
158
|
+
vars = []
|
159
|
+
object.cvars.each do |cvar|
|
160
|
+
return_tag = cvar.tag(:return)
|
161
|
+
vars << VarDefinition.new(
|
162
|
+
name: cvar.name,
|
163
|
+
source: "#{cvar.file}:#{cvar.line}",
|
164
|
+
scope: :class,
|
165
|
+
types: build_types(return_tag),
|
166
|
+
types_string: build_types_string(return_tag)
|
167
|
+
)
|
168
|
+
end
|
169
|
+
object.constants.each do |const|
|
170
|
+
return_tag = const.tag(:return)
|
171
|
+
vars << VarDefinition.new(
|
172
|
+
name: const.name,
|
173
|
+
source: "#{const.file}:#{const.line}",
|
174
|
+
scope: :constant,
|
175
|
+
types: build_types(return_tag),
|
176
|
+
types_string: build_types_string(return_tag)
|
177
|
+
)
|
178
|
+
end
|
179
|
+
object.attributes[:instance].each do |key, value|
|
180
|
+
method = value[:read]
|
181
|
+
return_tag = method.tag(:return)
|
182
|
+
vars << VarDefinition.new(
|
183
|
+
name: "@#{key}".to_sym,
|
184
|
+
source: "#{method.file}:#{method.line}",
|
185
|
+
scope: :instance,
|
186
|
+
types: build_types(return_tag),
|
187
|
+
types_string: build_types_string(return_tag)
|
188
|
+
)
|
189
|
+
end
|
190
|
+
object.attributes[:class].each do |key, value|
|
191
|
+
method = value[:read]
|
192
|
+
return_tag = method.tag(:return)
|
193
|
+
vars << VarDefinition.new(
|
194
|
+
name: key,
|
195
|
+
source: "#{method.file}:#{method.line}",
|
196
|
+
scope: :self,
|
197
|
+
types: build_types(return_tag),
|
198
|
+
types_string: build_types_string(return_tag)
|
199
|
+
)
|
200
|
+
end
|
201
|
+
|
202
|
+
vars
|
203
|
+
end
|
204
|
+
|
205
|
+
def build_types(tag)
|
206
|
+
if tag.respond_to?(:types) && tag.types && !tag.types.empty?
|
207
|
+
tag.types.map { |t| Typeguard::TypeModel::Mapper::YardMapper.parse_map(t) }
|
208
|
+
else
|
209
|
+
result = TypeNode.new(
|
210
|
+
kind: :untyped,
|
211
|
+
shape: :untyped,
|
212
|
+
children: [],
|
213
|
+
metadata: { note: 'Types specifier list is empty: untyped' }
|
214
|
+
)
|
215
|
+
[result]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_types_string(tag)
|
220
|
+
tag.respond_to?(:types) && !tag.types.nil? ? tag.types.join(' or ') : []
|
221
|
+
end
|
222
|
+
|
223
|
+
def build_symbol
|
224
|
+
Typeguard::TypeModel::Mapper::YardMapper.parse_map('Symbol')
|
225
|
+
end
|
226
|
+
|
227
|
+
def build_fixed_hash(children)
|
228
|
+
node = Typeguard::TypeModel::Mapper::YardMapper.parse_map('Hash')
|
229
|
+
node.shape = :fixed_hash
|
230
|
+
node.children = children
|
231
|
+
node.metadata[:note] = 'Hash specified via @options'
|
232
|
+
node
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typeguard
|
4
|
+
module TypeModel
|
5
|
+
module Builder
|
6
|
+
IMPL_SYM = :IMPLEMENTATION
|
7
|
+
def self.yard
|
8
|
+
require_relative 'builder/yard_builder'
|
9
|
+
require_relative 'mapper/yard_mapper'
|
10
|
+
const_set(IMPL_SYM, YardBuilder)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.rbs
|
14
|
+
require_relative 'builder/rbs_builder'
|
15
|
+
require_relative 'mapper/rbs_mapper'
|
16
|
+
const_set(IMPL_SYM, RBSBuilder)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typeguard
|
4
|
+
module TypeModel
|
5
|
+
module Definitions
|
6
|
+
TypeNode = Struct.new(:kind, :shape, :children, :metadata, keyword_init: true)
|
7
|
+
ModuleDefinition = Struct.new(:name, :source, :vars, :type_parameters, :children, keyword_init: true)
|
8
|
+
ClassDefinition = Struct.new(:name, :source, :vars, :parent, :type_parameters, :children, keyword_init: true)
|
9
|
+
MethodDefinition = Struct.new(:name, :source, :scope, :visibility, :parameters, :returns, keyword_init: true)
|
10
|
+
ParameterDefinition = Struct.new(:name, :source, :default, :types, :types_string, keyword_init: true)
|
11
|
+
ReturnDefinition = Struct.new(:source, :types, :types_string, keyword_init: true)
|
12
|
+
VarDefinition = Struct.new(:name, :source, :scope, :types, :types_string, keyword_init: true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rbs'
|
4
|
+
|
5
|
+
module Typeguard
|
6
|
+
module TypeModel
|
7
|
+
module Mapper
|
8
|
+
# Maps RBS types to the generic type model
|
9
|
+
class RBSMapper
|
10
|
+
include Typeguard::TypeModel::Definitions
|
11
|
+
|
12
|
+
# @param type [RBS::Types::Base] an RBS type object
|
13
|
+
# @return [TypeNode] a mapped type node in the generic model.
|
14
|
+
def self.parse_map(type)
|
15
|
+
map_rbs(type)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.map_rbs(node)
|
19
|
+
name = (node.respond_to?(:name) ? node.name.relative!.to_s : node.to_s).to_sym
|
20
|
+
mapped_node = TypeNode.new(kind: name, shape: :basic, children: [], metadata: {})
|
21
|
+
|
22
|
+
case node
|
23
|
+
when RBS::Types::ClassInstance
|
24
|
+
if node.args.empty?
|
25
|
+
mapped_node.shape = :basic
|
26
|
+
elsif node.name.name == :Hash && node.args.length == 2
|
27
|
+
key_node = map_rbs(node.args.first)
|
28
|
+
value_node = map_rbs(node.args.last)
|
29
|
+
mapped_node.shape = :hash
|
30
|
+
mapped_node.children = [[key_node], [value_node]]
|
31
|
+
mapped_node.metadata[:note] = 'Hash specified via parametrized types: one key and one value type'
|
32
|
+
else
|
33
|
+
children = node.args.map { |arg| map_rbs(arg) }
|
34
|
+
mapped_node.children = children
|
35
|
+
mapped_node.shape = :generic
|
36
|
+
end
|
37
|
+
when RBS::Types::Tuple
|
38
|
+
children = node.types.map { |child| map_rbs(child) }
|
39
|
+
mapped_node.kind = :Array
|
40
|
+
mapped_node.shape = :fixed
|
41
|
+
mapped_node.children = children
|
42
|
+
mapped_node.metadata[:note] = 'Tuples are a fixed-length array with known types for each element'
|
43
|
+
when RBS::Types::Union
|
44
|
+
children = node.types.map { |child| map_rbs(child) }
|
45
|
+
mapped_node.shape = :union
|
46
|
+
mapped_node.children = children
|
47
|
+
mapped_node.metadata[:note] = 'Union types denote a type of one of the given types'
|
48
|
+
when RBS::Types::Optional
|
49
|
+
child = map_rbs(node.type)
|
50
|
+
nil_node = TypeNode.new(kind: :nil, shape: :literal, children: [], metadata: { note: 'Optional nil' })
|
51
|
+
mapped_node.shape = :union
|
52
|
+
mapped_node.children = [child, nil_node]
|
53
|
+
mapped_node.metadata[:note] = 'Optional type'
|
54
|
+
when RBS::Types::Bases::Bool
|
55
|
+
true_node = TypeNode.new(kind: :TrueClass, shape: :basic, children: [], metadata: {})
|
56
|
+
false_node = TypeNode.new(kind: :FalseClass, shape: :basic, children: [], metadata: {})
|
57
|
+
mapped_node.kind = :boolean
|
58
|
+
mapped_node.shape = :union
|
59
|
+
mapped_node.children = [true_node, false_node]
|
60
|
+
mapped_node.metadata = { note: 'Boolean represents both TrueClass and FalseClass' }
|
61
|
+
when RBS::Types::Bases::Any
|
62
|
+
mapped_node.kind = :untyped
|
63
|
+
mapped_node.shape = :untyped
|
64
|
+
mapped_node.metadata[:note] = 'Type not specified: any'
|
65
|
+
when RBS::Types::Bases::Self
|
66
|
+
mapped_node.shape = :literal
|
67
|
+
mapped_node.metadata[:note] = 'self indicates that calling this method on will return the same type as the type of the receiver'
|
68
|
+
when RBS::Types::Bases::Void
|
69
|
+
mapped_node.shape = :literal
|
70
|
+
mapped_node.metadata[:note] = 'void is only allowed as a return type or a generic parameter'
|
71
|
+
when RBS::Types::Bases::Nil
|
72
|
+
mapped_node.shape = :literal
|
73
|
+
mapped_node.metadata[:note] = 'nil is for nil. nil is recommended over NilClass'
|
74
|
+
when RBS::Types::Literal
|
75
|
+
mapped_node.shape = :literal
|
76
|
+
mapped_node.metadata[:note] = 'Literal types denote a type with only one value of the literal'
|
77
|
+
else raise "Unknown node type: #{node.class} #{node}"
|
78
|
+
end
|
79
|
+
mapped_node
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typeguard
|
4
|
+
module TypeModel
|
5
|
+
module Mapper
|
6
|
+
# Maps YARD types to the type model
|
7
|
+
class YardMapper
|
8
|
+
include Typeguard::TypeModel::Definitions
|
9
|
+
|
10
|
+
SPECIAL_LITERALS = %w[true false nil self void].freeze
|
11
|
+
|
12
|
+
def self.parse_map(type)
|
13
|
+
# NOTE: Using private YARD API here.
|
14
|
+
map_yard(YARD::Tags::TypesExplainer::Parser.parse(type).first)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.map_yard(node)
|
18
|
+
mapped_node = TypeNode.new(kind: node.name.to_sym, shape: :basic, children: [], metadata: {})
|
19
|
+
case node
|
20
|
+
when YARD::Tags::TypesExplainer::FixedCollectionType
|
21
|
+
children = node.types.map { |child| map_yard(child) }
|
22
|
+
mapped_node.shape = :fixed
|
23
|
+
mapped_node.children = children
|
24
|
+
mapped_node.metadata[:note] = 'Order-dependent lists must appear in the exact order'
|
25
|
+
when YARD::Tags::TypesExplainer::CollectionType
|
26
|
+
if node.name == 'Hash' && node.types.length == 2
|
27
|
+
key_node = map_yard(node.types.first)
|
28
|
+
value_node = map_yard(node.types.last)
|
29
|
+
mapped_node.shape = :hash
|
30
|
+
mapped_node.children = [[key_node], [value_node]]
|
31
|
+
mapped_node.metadata[:note] = 'Hash specified via parametrized types: one key and one value type'
|
32
|
+
else
|
33
|
+
children = node.types.map { |child| map_yard(child) }
|
34
|
+
mapped_node.shape = :generic
|
35
|
+
mapped_node.children = children
|
36
|
+
mapped_node.metadata[:note] = 'Generics specify one or more parametrized types'
|
37
|
+
end
|
38
|
+
when YARD::Tags::TypesExplainer::HashCollectionType
|
39
|
+
key_nodes = node.key_types.map { |k| map_yard(k) }
|
40
|
+
value_nodes = node.value_types.map { |v| map_yard(v) }
|
41
|
+
mapped_node.shape = :hash
|
42
|
+
mapped_node.children = [key_nodes, value_nodes]
|
43
|
+
mapped_node.metadata[:note] = 'Hash specified via rocket syntax: multiple key and value type'
|
44
|
+
when YARD::Tags::TypesExplainer::Type
|
45
|
+
map_base(node, mapped_node)
|
46
|
+
else raise "Unknown node type: #{node.class}"
|
47
|
+
end
|
48
|
+
mapped_node
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.map_base(node, mapped_node)
|
52
|
+
case node.name
|
53
|
+
when *SPECIAL_LITERALS
|
54
|
+
mapped_node.shape = :literal
|
55
|
+
mapped_node.metadata = { note: 'Ruby (and YARD) supported literal' }
|
56
|
+
when 'Boolean'
|
57
|
+
true_node = TypeNode.new(kind: :TrueClass, shape: :basic, children: [], metadata: {})
|
58
|
+
false_node = TypeNode.new(kind: :FalseClass, shape: :basic, children: [], metadata: {})
|
59
|
+
mapped_node.kind = :boolean
|
60
|
+
mapped_node.shape = :union
|
61
|
+
mapped_node.children = [true_node, false_node]
|
62
|
+
mapped_node.metadata = { note: 'Boolean represents both TrueClass and FalseClass' }
|
63
|
+
when /^#\w+/
|
64
|
+
mapped_node.shape = :duck
|
65
|
+
mapped_node.metadata = { note: "Duck-type: object should respond to :#{node.name[1..]}" }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typeguard
|
4
|
+
module Validation
|
5
|
+
class Base
|
6
|
+
def self.from(node)
|
7
|
+
case node.shape
|
8
|
+
when :basic
|
9
|
+
Basic.new(node)
|
10
|
+
when :generic
|
11
|
+
Generic.new(node)
|
12
|
+
when :fixed
|
13
|
+
Fixed.new(node)
|
14
|
+
when :hash
|
15
|
+
GenericHash.new(node)
|
16
|
+
when :fixed_hash
|
17
|
+
FixedHash.new(node)
|
18
|
+
when :union
|
19
|
+
Union.new(node)
|
20
|
+
when :literal
|
21
|
+
case node.kind
|
22
|
+
when :nil then Nil.new
|
23
|
+
when :void, :self then Untyped.new
|
24
|
+
else Literal.new(node)
|
25
|
+
end
|
26
|
+
when :duck
|
27
|
+
Duck.new(node)
|
28
|
+
when :untyped
|
29
|
+
Untyped.new
|
30
|
+
else
|
31
|
+
raise "Unexpected type node shape: #{node.shape}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?(_value)
|
36
|
+
raise NotImplementedError, 'Abstract'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Basic < Base
|
41
|
+
def initialize(node)
|
42
|
+
@klass = node.metadata[:const]
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid?(value)
|
46
|
+
value.is_a?(@klass)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Generic < Base
|
51
|
+
def initialize(node)
|
52
|
+
@klass = node.metadata[:const]
|
53
|
+
@children = node.children.map { |child| Base.from(child) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def valid?(value)
|
57
|
+
return false unless value.is_a?(@klass)
|
58
|
+
|
59
|
+
value.all? do |element|
|
60
|
+
@children.any? { |child| child.valid?(element) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Fixed < Base
|
66
|
+
def initialize(node)
|
67
|
+
@klass = node.metadata[:const]
|
68
|
+
@children = node.children.map { |child| Base.from(child) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def valid?(value)
|
72
|
+
return false unless value.is_a?(@klass)
|
73
|
+
return false unless value.size == @children.size
|
74
|
+
|
75
|
+
@children.each_with_index.all? do |child, i|
|
76
|
+
child.valid?(value[i])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class GenericHash < Base
|
82
|
+
def initialize(node)
|
83
|
+
@klass = node.metadata[:const]
|
84
|
+
@keys = node.children.first.map { |k| Base.from(k) }
|
85
|
+
@values = node.children.last.map { |v| Base.from(v) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def valid?(value)
|
89
|
+
return false unless value.is_a?(@klass)
|
90
|
+
|
91
|
+
value.all? do |k, v|
|
92
|
+
key_valid = @keys.any? { |child| child.valid?(k) }
|
93
|
+
value_valid = @values.any? { |child| child.valid?(v) }
|
94
|
+
key_valid && value_valid
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class FixedHash < Base
|
100
|
+
def initialize(node)
|
101
|
+
@klass = node.metadata[:const]
|
102
|
+
@map = node.children.transpose.each_with_object({}) do |(k, v), h|
|
103
|
+
index = k.metadata[:key]
|
104
|
+
k_validator = Base.from(k)
|
105
|
+
v_validators = v.map { |val| Base.from(val) }
|
106
|
+
h[index] = [k_validator, v_validators]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def valid?(value)
|
111
|
+
return false unless value.is_a?(@klass)
|
112
|
+
|
113
|
+
value.all? do |k, v|
|
114
|
+
index = k.to_s
|
115
|
+
return false unless @map.key?(index)
|
116
|
+
|
117
|
+
k_validator, v_validator = @map[index]
|
118
|
+
key_valid = k_validator.valid?(k)
|
119
|
+
value_valid = v_validator.any? { |child| child.valid?(v) }
|
120
|
+
key_valid && value_valid
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Union < Base
|
126
|
+
def initialize(node)
|
127
|
+
@children = node.children.map { |child| Base.from(child) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def valid?(value)
|
131
|
+
@children.any? { |child| child.valid?(value) }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class Literal < Base
|
136
|
+
def initialize(node)
|
137
|
+
@expected = node.kind.to_s
|
138
|
+
end
|
139
|
+
|
140
|
+
def valid?(value)
|
141
|
+
value.to_s == @expected
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Nil < Base
|
146
|
+
def valid?(value)
|
147
|
+
value.nil?
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class Duck < Base
|
152
|
+
def initialize(node)
|
153
|
+
@name = node.kind[1..]
|
154
|
+
end
|
155
|
+
|
156
|
+
def valid?(value)
|
157
|
+
value.respond_to?(@name)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
class Untyped < Base
|
162
|
+
def valid?(_)
|
163
|
+
true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class UnionOf < Base
|
168
|
+
def initialize(children)
|
169
|
+
@children = children
|
170
|
+
end
|
171
|
+
|
172
|
+
def valid?(value)
|
173
|
+
@children.any? { |v| v.valid?(value) }
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Typeguard
|
4
|
+
module Validation
|
5
|
+
class Validator
|
6
|
+
def self.param_validator(types)
|
7
|
+
children = types.map { |node| Base.from(node) }
|
8
|
+
if children.size == 1
|
9
|
+
children.first
|
10
|
+
else
|
11
|
+
UnionOf.new(children)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.zip_params(method_params, sig_params)
|
16
|
+
method_params.map do |mp|
|
17
|
+
sig_param = sig_params.find { |sp| sp.name.to_s.gsub(/[*:]/, '').to_sym == mp.last }
|
18
|
+
validator = param_validator(sig_param.types) if sig_param
|
19
|
+
[mp, sig_param, validator]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.param_names(zipped_params)
|
24
|
+
# Tuples of [parameter, invocation] names
|
25
|
+
zipped_params.map do |(type, name), sp, _|
|
26
|
+
name = name.to_s
|
27
|
+
case type
|
28
|
+
when :req then [name, name] # foo
|
29
|
+
when :keyreq then ["#{name}:", "#{name}: #{name}"] # foo:
|
30
|
+
when :keyrest then [name == '**' ? name : "**#{name}"] * 2 # **foo
|
31
|
+
when :rest then [name == '*' ? name : "*#{name}"] * 2 # *foo
|
32
|
+
when :block then [name == '&' ? name : "&#{name}"] * 2 # &foo
|
33
|
+
when :opt then ["#{name} = (#{sp.default})", name] # foo = (bar)
|
34
|
+
when :key then ["#{name}: (#{sp.default})", "#{name}: #{name}"] # foo: (bar)
|
35
|
+
when :nokey then ['**nil'] * 2 # **nil
|
36
|
+
else raise type.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.exhaustive_path(mod, method, sig)
|
42
|
+
zipped_params = zip_params(method.parameters, sig.parameters)
|
43
|
+
return_validator = (param_validator(sig.returns.types) if sig.returns && !sig.returns.types.empty?)
|
44
|
+
p_names = param_names(zipped_params)
|
45
|
+
block_params = p_names.map(&:first).join(', ')
|
46
|
+
call_args = p_names.map(&:last).reject { |s| ['*', '**', '&'].include?(s) }.join(', ')
|
47
|
+
locals = method.parameters.map do |s|
|
48
|
+
return nil if s.size == 1 # Ruby 3.1.0 compatible
|
49
|
+
|
50
|
+
['*', '**', '&'].include?(s.last.to_s) ? nil : s.last
|
51
|
+
end.compact.join(', ')
|
52
|
+
redefinition = sig.scope == :class ? 'define_singleton_method' : 'define_method'
|
53
|
+
if return_validator
|
54
|
+
mod.module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
55
|
+
#{redefinition}(sig.name) do |#{block_params}|
|
56
|
+
zipped_params.zip([#{locals}]).each do |(mp, sp, validator), local|
|
57
|
+
next if validator.nil? || validator.valid?(local)
|
58
|
+
|
59
|
+
Metrics.report_unexpected_argument(sig, sp.types_string, local, mod.name, sp)
|
60
|
+
end
|
61
|
+
result = method.bind_call(self, #{call_args})
|
62
|
+
unless return_validator.valid?(result)
|
63
|
+
Metrics.report_unexpected_return(sig, sig.returns.types_string, result, mod.name)
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
RUBY
|
68
|
+
else
|
69
|
+
mod.module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
70
|
+
#{redefinition}(sig.name) do |#{block_params}|
|
71
|
+
zipped_params.zip([#{locals}]).each do |(mp, sp, validator), local|
|
72
|
+
next if validator.nil? || validator.valid?(local)
|
73
|
+
|
74
|
+
Metrics.report_unexpected_argument(sig, sp.types_string, local, mod.name, sp)
|
75
|
+
end
|
76
|
+
method.bind_call(self, #{call_args})
|
77
|
+
end
|
78
|
+
RUBY
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|