docscribe 1.4.1 → 1.4.2
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 +4 -4
- data/README.md +149 -0
- data/lib/docscribe/cli/config_builder.rb +125 -35
- data/lib/docscribe/cli/generate.rb +288 -117
- data/lib/docscribe/cli/init.rb +49 -13
- data/lib/docscribe/cli/options.rb +302 -127
- data/lib/docscribe/cli/run.rb +391 -135
- data/lib/docscribe/cli.rb +23 -5
- data/lib/docscribe/config/defaults.rb +11 -11
- data/lib/docscribe/config/emit.rb +1 -0
- data/lib/docscribe/config/filtering.rb +24 -11
- data/lib/docscribe/config/loader.rb +7 -4
- data/lib/docscribe/config/plugin.rb +1 -0
- data/lib/docscribe/config/rbs.rb +31 -22
- data/lib/docscribe/config/sorbet.rb +41 -15
- data/lib/docscribe/config/sorting.rb +1 -0
- data/lib/docscribe/config/template.rb +1 -0
- data/lib/docscribe/config/utils.rb +1 -0
- data/lib/docscribe/config.rb +1 -0
- data/lib/docscribe/infer/constants.rb +15 -0
- data/lib/docscribe/infer/literals.rb +43 -25
- data/lib/docscribe/infer/names.rb +24 -15
- data/lib/docscribe/infer/params.rb +52 -6
- data/lib/docscribe/infer/raises.rb +24 -14
- data/lib/docscribe/infer/returns.rb +365 -182
- data/lib/docscribe/infer.rb +10 -9
- data/lib/docscribe/inline_rewriter/collector.rb +766 -375
- data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
- data/lib/docscribe/inline_rewriter/doc_builder.rb +1488 -602
- data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
- data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
- data/lib/docscribe/inline_rewriter.rb +1009 -595
- data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
- data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
- data/lib/docscribe/plugin/registry.rb +34 -7
- data/lib/docscribe/plugin.rb +48 -17
- data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
- data/lib/docscribe/types/rbs/provider.rb +75 -26
- data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
- data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
- data/lib/docscribe/version.rb +1 -1
- metadata +2 -2
|
@@ -6,6 +6,15 @@ module Docscribe
|
|
|
6
6
|
module Returns
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
+
LAST_EXPR_TYPE_HANDLERS = {
|
|
10
|
+
begin: :handle_begin_node,
|
|
11
|
+
if: :handle_if_node,
|
|
12
|
+
case: :handle_case_node,
|
|
13
|
+
return: :handle_return_node,
|
|
14
|
+
block: :handle_block_node,
|
|
15
|
+
send: :handle_send_node
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
9
18
|
# Infer a return type from a full method definition source string.
|
|
10
19
|
#
|
|
11
20
|
# The source must parse to a `:def` or `:defs` node. If parsing fails or inference
|
|
@@ -18,36 +27,40 @@ module Docscribe
|
|
|
18
27
|
def infer_return_type(method_source)
|
|
19
28
|
return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
buffer.source = method_source
|
|
23
|
-
root = Docscribe::Parsing.parse_buffer(buffer)
|
|
30
|
+
root = parse_method_source(method_source)
|
|
24
31
|
return FALLBACK_TYPE unless root && %i[def defs].include?(root.type)
|
|
25
32
|
|
|
26
33
|
body = root.children.last
|
|
27
34
|
local_var_types = build_local_variable_types(body)
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
|
|
36
|
+
local_var_types: local_var_types) || FALLBACK_TYPE
|
|
30
37
|
rescue Parser::SyntaxError
|
|
31
38
|
FALLBACK_TYPE
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
# Parse a Ruby source string into an AST using the Parser gem.
|
|
42
|
+
#
|
|
43
|
+
# @note module_function: when included, also defines #parse_method_source (instance visibility: private)
|
|
44
|
+
# @param [String] method_source the method definition source string to parse
|
|
45
|
+
# @return [Parser::AST::Node, nil]
|
|
46
|
+
def parse_method_source(method_source)
|
|
47
|
+
buffer = Parser::Source::Buffer.new('(method)')
|
|
48
|
+
buffer.source = method_source
|
|
49
|
+
Docscribe::Parsing.parse_buffer(buffer)
|
|
50
|
+
end
|
|
51
|
+
|
|
34
52
|
# Infer a method's normal return type from an already parsed def/defs node.
|
|
35
53
|
#
|
|
36
54
|
# @note module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
|
|
37
55
|
# @param [Parser::AST::Node] node `:def` or `:defs` node
|
|
38
56
|
# @return [String]
|
|
39
57
|
def infer_return_type_from_node(node)
|
|
40
|
-
body =
|
|
41
|
-
case node.type
|
|
42
|
-
when :def then node.children[2]
|
|
43
|
-
when :defs then node.children[3]
|
|
44
|
-
end
|
|
45
|
-
|
|
58
|
+
body = extract_def_body(node)
|
|
46
59
|
return FALLBACK_TYPE unless body
|
|
47
60
|
|
|
48
61
|
local_var_types = build_local_variable_types(body)
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
|
|
63
|
+
local_var_types: local_var_types) || FALLBACK_TYPE
|
|
51
64
|
end
|
|
52
65
|
|
|
53
66
|
# Return a structured return-type spec for a method node.
|
|
@@ -60,82 +73,313 @@ module Docscribe
|
|
|
60
73
|
# @param [Parser::AST::Node] node `:def` or `:defs` node
|
|
61
74
|
# @param [String] fallback_type type used when inference is uncertain
|
|
62
75
|
# @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
|
|
63
|
-
# @param [nil] core_rbs_provider
|
|
64
|
-
# @param [nil] param_types
|
|
76
|
+
# @param [nil] core_rbs_provider core RBS type lookup provider
|
|
77
|
+
# @param [nil] param_types parameter name -> type map
|
|
65
78
|
# @return [Hash]
|
|
66
79
|
def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
|
|
67
80
|
param_types: nil)
|
|
68
|
-
body =
|
|
69
|
-
|
|
70
|
-
when :def then node.children[2]
|
|
71
|
-
when :defs then node.children[3]
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
spec = { normal: FALLBACK_TYPE, rescues: [] }
|
|
81
|
+
body = extract_def_body(node)
|
|
82
|
+
spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
|
|
75
83
|
return spec unless body
|
|
76
84
|
|
|
77
85
|
local_var_types = build_local_variable_types(body)
|
|
78
86
|
|
|
87
|
+
populate_returns_spec(spec, body, local_var_types,
|
|
88
|
+
fallback_type: fallback_type,
|
|
89
|
+
nil_as_optional: nil_as_optional,
|
|
90
|
+
core_rbs_provider: core_rbs_provider,
|
|
91
|
+
param_types: param_types)
|
|
92
|
+
|
|
93
|
+
spec
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Extract the body child node from a `:def` or `:defs` AST node.
|
|
97
|
+
#
|
|
98
|
+
# @note module_function: when included, also defines #extract_def_body (instance visibility: private)
|
|
99
|
+
# @param [Parser::AST::Node] node a `:def` or `:defs` AST node
|
|
100
|
+
# @return [Parser::AST::Node, nil]
|
|
101
|
+
def extract_def_body(node)
|
|
102
|
+
case node.type
|
|
103
|
+
when :def then node.children[2]
|
|
104
|
+
when :defs then node.children[3]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Populate the spec hash with normal and/or rescue return types from the body.
|
|
109
|
+
#
|
|
110
|
+
# @note module_function: when included, also defines #populate_returns_spec (instance visibility: private)
|
|
111
|
+
# @param [Hash] spec the return spec hash to populate
|
|
112
|
+
# @param [Parser::AST::Node] body the method body AST node
|
|
113
|
+
# @param [Hash, nil] local_var_types inferred local variable type map
|
|
114
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
115
|
+
# @return [Hash]
|
|
116
|
+
def populate_returns_spec(spec, body, local_var_types, **opts)
|
|
79
117
|
if body.type == :rescue
|
|
80
|
-
|
|
81
|
-
rescue_local_var_types = build_local_variable_types(body)
|
|
82
|
-
all_local_var_types = rescue_local_var_types || local_var_types
|
|
83
|
-
spec[:normal] =
|
|
84
|
-
last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
85
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
86
|
-
local_var_types: all_local_var_types) || FALLBACK_TYPE
|
|
87
|
-
|
|
88
|
-
body.children.each do |ch|
|
|
89
|
-
next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
|
|
90
|
-
|
|
91
|
-
exc_list, _asgn, rescue_body = *ch
|
|
92
|
-
exc_names = Raises.exception_names_from_rescue_list(exc_list)
|
|
93
|
-
rtype =
|
|
94
|
-
last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
95
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
96
|
-
local_var_types: all_local_var_types) ||
|
|
97
|
-
fallback_type
|
|
98
|
-
spec[:rescues] << [exc_names, rtype]
|
|
99
|
-
end
|
|
118
|
+
process_rescue_body(spec, body, **opts)
|
|
100
119
|
else
|
|
101
|
-
spec[:normal] =
|
|
102
|
-
last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
103
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
104
|
-
local_var_types: local_var_types) || FALLBACK_TYPE
|
|
120
|
+
spec[:normal] = infer_normal_return_type(body, **opts, local_var_types: local_var_types)
|
|
105
121
|
end
|
|
122
|
+
end
|
|
106
123
|
|
|
107
|
-
|
|
124
|
+
# Infer the normal (non-rescue) return type from a method body node.
|
|
125
|
+
#
|
|
126
|
+
# @note module_function: when included, also defines #infer_normal_return_type (instance visibility: private)
|
|
127
|
+
# @param [Parser::AST::Node] body the method body AST node
|
|
128
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
129
|
+
# @return [String]
|
|
130
|
+
def infer_normal_return_type(body, **opts)
|
|
131
|
+
run_last_expr_type(body, **opts) || FALLBACK_TYPE
|
|
108
132
|
end
|
|
109
133
|
|
|
110
|
-
#
|
|
134
|
+
# Process a :rescue body node and populate spec with normal + rescue return types.
|
|
111
135
|
#
|
|
112
|
-
# @note module_function: when included, also defines #
|
|
113
|
-
# @
|
|
114
|
-
# @param [
|
|
115
|
-
# @
|
|
136
|
+
# @note module_function: when included, also defines #process_rescue_body (instance visibility: private)
|
|
137
|
+
# @param [Hash] spec the return spec hash to populate
|
|
138
|
+
# @param [Parser::AST::Node] body the :rescue AST node
|
|
139
|
+
# @param [String] fallback_type type used when inference is uncertain
|
|
140
|
+
# @param [Boolean] nil_as_optional whether nil unions render as optional types
|
|
141
|
+
# @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
|
|
142
|
+
# @param [Hash, nil] param_types parameter name to type map
|
|
143
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
144
|
+
# @return [Hash]
|
|
145
|
+
def process_rescue_body(spec, body, **opts)
|
|
146
|
+
main_body = body.children[0]
|
|
147
|
+
local_var_types = build_local_variable_types(body)
|
|
148
|
+
rescue_opts = opts.merge(local_var_types: local_var_types)
|
|
149
|
+
spec[:normal] = run_last_expr_type(main_body, **rescue_opts) || FALLBACK_TYPE
|
|
150
|
+
process_rescue_branches(spec, body, **rescue_opts)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Extract return types from each :resbody child and append to spec[:rescues].
|
|
154
|
+
#
|
|
155
|
+
# @note module_function: when included, also defines #process_rescue_branches (instance visibility: private)
|
|
156
|
+
# @param [Hash] spec the return spec hash to populate
|
|
157
|
+
# @param [Parser::AST::Node] body the :rescue AST node
|
|
158
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
159
|
+
# @return [Array] the list of rescue type entries
|
|
160
|
+
def process_rescue_branches(spec, body, **opts)
|
|
161
|
+
body.children.each do |ch|
|
|
162
|
+
next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
|
|
163
|
+
|
|
164
|
+
exc_list, _asgn, rescue_body = *ch
|
|
165
|
+
exc_names = Raises.exception_names_from_rescue_list(exc_list)
|
|
166
|
+
rtype = run_last_expr_type(rescue_body, **opts) || opts[:fallback_type]
|
|
167
|
+
spec[:rescues] << [exc_names, rtype]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Build a map of local/global/ivar/constant assignments to inferred types.
|
|
172
|
+
#
|
|
173
|
+
# @note module_function: when included, also defines #build_local_variable_types (instance visibility: private)
|
|
174
|
+
# @param [Parser::AST::Node] node AST node to walk
|
|
175
|
+
# @return [Hash, nil]
|
|
116
176
|
def build_local_variable_types(node)
|
|
117
|
-
types = {}
|
|
177
|
+
types = {} #: Hash[String, String]
|
|
118
178
|
ASTWalk.walk(node) do |n|
|
|
119
|
-
|
|
120
|
-
when :lvasgn, :gvasgn, :ivasgn
|
|
121
|
-
name = n.children[0].to_s
|
|
122
|
-
value = n.children[1]
|
|
123
|
-
if value
|
|
124
|
-
inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
|
|
125
|
-
types[name] = inferred if inferred && inferred != FALLBACK_TYPE
|
|
126
|
-
end
|
|
127
|
-
when :casgn
|
|
128
|
-
name = n.children[0].to_s
|
|
129
|
-
value = n.children[2]
|
|
130
|
-
if value
|
|
131
|
-
inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
|
|
132
|
-
types[name] = inferred if inferred && inferred != FALLBACK_TYPE
|
|
133
|
-
end
|
|
134
|
-
end
|
|
179
|
+
collect_assignment_type(n, types)
|
|
135
180
|
end
|
|
136
181
|
types.empty? ? nil : types
|
|
137
182
|
end
|
|
138
183
|
|
|
184
|
+
# Infer the type of a single assignment node and store it in the types hash.
|
|
185
|
+
#
|
|
186
|
+
# @note module_function: when included, also defines #collect_assignment_type (instance visibility: private)
|
|
187
|
+
# @param [Parser::AST::Node] node an assignment AST node
|
|
188
|
+
# @param [Hash] types the accumulated local variable type map
|
|
189
|
+
# @return [void]
|
|
190
|
+
def collect_assignment_type(node, types)
|
|
191
|
+
name, value = assignment_name_and_value(node)
|
|
192
|
+
return unless name && value
|
|
193
|
+
|
|
194
|
+
inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
|
|
195
|
+
types[name] = inferred if inferred && inferred != FALLBACK_TYPE
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Extract the variable name and value expression from an assignment node.
|
|
199
|
+
#
|
|
200
|
+
# @note module_function: when included, also defines #assignment_name_and_value (instance visibility: private)
|
|
201
|
+
# @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn)
|
|
202
|
+
# @return [Array<(String, Parser::AST::Node)>] pair of variable name and value node
|
|
203
|
+
def assignment_name_and_value(node)
|
|
204
|
+
case node.type
|
|
205
|
+
when :lvasgn, :gvasgn, :ivasgn
|
|
206
|
+
[node.children[0].to_s, node.children[1]]
|
|
207
|
+
when :casgn
|
|
208
|
+
[node.children[0].to_s, node.children[2]]
|
|
209
|
+
else
|
|
210
|
+
[nil, nil]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Handle `:begin` node for last_expr_type.
|
|
215
|
+
#
|
|
216
|
+
# @note module_function: when included, also defines #handle_begin_node (instance visibility: private)
|
|
217
|
+
# @param [Object] node
|
|
218
|
+
# @param [Hash] opts
|
|
219
|
+
# @return [Object]
|
|
220
|
+
def handle_begin_node(node, **opts)
|
|
221
|
+
run_last_expr_type(node.children.last, **opts)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Handle `:if` node for last_expr_type.
|
|
225
|
+
#
|
|
226
|
+
# @note module_function: when included, also defines #handle_if_node (instance visibility: private)
|
|
227
|
+
# @param [Object] node
|
|
228
|
+
# @param [Hash] opts
|
|
229
|
+
# @return [Object]
|
|
230
|
+
def handle_if_node(node, **opts)
|
|
231
|
+
t = run_last_expr_type(node.children[1], **opts)
|
|
232
|
+
e = run_last_expr_type(node.children[2], **opts)
|
|
233
|
+
unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
|
|
234
|
+
nil_as_optional: opts.fetch(:nil_as_optional, true))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Handle `:case` node for last_expr_type.
|
|
238
|
+
#
|
|
239
|
+
# @note module_function: when included, also defines #handle_case_node (instance visibility: private)
|
|
240
|
+
# @param [Object] node
|
|
241
|
+
# @param [Hash] opts
|
|
242
|
+
# @return [Object]
|
|
243
|
+
def handle_case_node(node, **opts)
|
|
244
|
+
branches = process_case_branches(node, **opts)
|
|
245
|
+
if branches.empty?
|
|
246
|
+
opts[:fallback_type]
|
|
247
|
+
else
|
|
248
|
+
branches.reduce do |a, b|
|
|
249
|
+
unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
|
|
250
|
+
nil_as_optional: opts.fetch(:nil_as_optional, true))
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Extract inferred return types from all branches of a :case expression.
|
|
256
|
+
#
|
|
257
|
+
# @note module_function: when included, also defines #process_case_branches (instance visibility: private)
|
|
258
|
+
# @param [Parser::AST::Node] node the :case AST node
|
|
259
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
260
|
+
# @return [Array<String>] list of inferred types from each branch
|
|
261
|
+
def process_case_branches(node, **opts)
|
|
262
|
+
(node.children[1..] || []).compact.flat_map do |child|
|
|
263
|
+
if child.type == :when
|
|
264
|
+
run_last_expr_type(child.children.last, **opts)
|
|
265
|
+
else
|
|
266
|
+
run_last_expr_type(child, **opts)
|
|
267
|
+
end
|
|
268
|
+
end.compact
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Handle `:block` node for last_expr_type.
|
|
272
|
+
#
|
|
273
|
+
# @note module_function: when included, also defines #handle_block_node (instance visibility: private)
|
|
274
|
+
# @param [Object] node
|
|
275
|
+
# @param [Hash] opts
|
|
276
|
+
# @return [Object]
|
|
277
|
+
def handle_block_node(node, **opts)
|
|
278
|
+
send_node = node.children[0]
|
|
279
|
+
if send_node&.type == :send
|
|
280
|
+
recv = send_node.children[0]
|
|
281
|
+
meth = send_node.children[1]
|
|
282
|
+
rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
|
|
283
|
+
opts[:param_types])
|
|
284
|
+
return rbs_type if rbs_type
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
run_last_expr_type(node.children[2], **opts)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Handle `:send` node for last_expr_type.
|
|
291
|
+
#
|
|
292
|
+
# @note module_function: when included, also defines #handle_send_node (instance visibility: private)
|
|
293
|
+
# @param [Object] node
|
|
294
|
+
# @param [Hash] opts
|
|
295
|
+
# @return [Object]
|
|
296
|
+
def handle_send_node(node, **opts)
|
|
297
|
+
recv = node.children[0]
|
|
298
|
+
meth = node.children[1]
|
|
299
|
+
|
|
300
|
+
if opts[:core_rbs_provider]
|
|
301
|
+
rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
|
|
302
|
+
opts[:param_types])
|
|
303
|
+
return rbs_type if rbs_type
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Resolve RBS return type for a send node's receiver, if possible.
|
|
310
|
+
#
|
|
311
|
+
# Handles `:lvar` and chained `:send` receivers.
|
|
312
|
+
#
|
|
313
|
+
# @note module_function: when included, also defines #resolve_rbs_for_send (instance visibility: private)
|
|
314
|
+
# @param [Parser::AST::Node, nil] recv the receiver node of the send
|
|
315
|
+
# @param [Symbol] meth the method name being called
|
|
316
|
+
# @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
|
|
317
|
+
# @param [Hash, nil] local_var_types inferred local variable type map
|
|
318
|
+
# @param [Hash, nil] param_types parameter name to type map
|
|
319
|
+
# @return [String, nil] resolved type or nil if unresolvable
|
|
320
|
+
def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
|
|
321
|
+
return nil unless core_rbs_provider
|
|
322
|
+
|
|
323
|
+
if recv&.type == :lvar
|
|
324
|
+
resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
|
|
325
|
+
elsif recv&.type == :send
|
|
326
|
+
resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Resolve RBS return type for an `:lvar` receiver.
|
|
331
|
+
#
|
|
332
|
+
# @note module_function: when included, also defines # (instance visibility: private)
|
|
333
|
+
# @private
|
|
334
|
+
# @param [Object] recv
|
|
335
|
+
# @param [Object] meth
|
|
336
|
+
# @param [Object] core_rbs_provider
|
|
337
|
+
# @param [Object] local_var_types
|
|
338
|
+
# @param [Object] param_types
|
|
339
|
+
# @return [String, nil]
|
|
340
|
+
def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
|
|
341
|
+
lvar_name = recv&.children&.first
|
|
342
|
+
recv_type = lookup_lvar_type(lvar_name, local_var_types, param_types)
|
|
343
|
+
return nil unless recv_type
|
|
344
|
+
|
|
345
|
+
rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
|
|
346
|
+
rbs_type unless rbs_type == FALLBACK_TYPE
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Look up a local variable's inferred type from local or parameter type maps.
|
|
350
|
+
#
|
|
351
|
+
# @note module_function: when included, also defines #lookup_lvar_type (instance visibility: private)
|
|
352
|
+
# @param [Symbol] lvar_name the local variable name
|
|
353
|
+
# @param [Hash, nil] local_var_types inferred local variable type map
|
|
354
|
+
# @param [Hash, nil] param_types parameter name to type map
|
|
355
|
+
# @return [String, nil]
|
|
356
|
+
def lookup_lvar_type(lvar_name, local_var_types, param_types)
|
|
357
|
+
return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
|
|
358
|
+
return param_types[lvar_name.to_s] if param_types&.key?(lvar_name.to_s)
|
|
359
|
+
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Resolve RBS return type for a chained `:send` receiver.
|
|
364
|
+
#
|
|
365
|
+
# @note module_function: when included, also defines # (instance visibility: private)
|
|
366
|
+
# @private
|
|
367
|
+
# @param [Object] recv
|
|
368
|
+
# @param [Object] meth
|
|
369
|
+
# @param [Object] core_rbs_provider
|
|
370
|
+
# @param [Object] local_var_types
|
|
371
|
+
# @param [Object] param_types
|
|
372
|
+
# @return [String, nil]
|
|
373
|
+
def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
|
|
374
|
+
inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
|
|
375
|
+
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
376
|
+
local_var_types: local_var_types)
|
|
377
|
+
return nil unless inner_type
|
|
378
|
+
|
|
379
|
+
rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
|
|
380
|
+
rbs_type unless rbs_type == FALLBACK_TYPE
|
|
381
|
+
end
|
|
382
|
+
|
|
139
383
|
# Infer the type of the last expression in a node.
|
|
140
384
|
#
|
|
141
385
|
# Supports:
|
|
@@ -152,122 +396,47 @@ module Docscribe
|
|
|
152
396
|
# @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
|
|
153
397
|
# @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
|
|
154
398
|
# @param [Hash, nil] param_types parameter name -> type map for lvar resolution
|
|
155
|
-
# @param [nil] local_var_types
|
|
399
|
+
# @param [nil] local_var_types pre-built local variable types map
|
|
400
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
156
401
|
# @return [String, nil]
|
|
157
|
-
def last_expr_type(node,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
case node.type
|
|
162
|
-
when :begin
|
|
163
|
-
last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
164
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
165
|
-
local_var_types: local_var_types)
|
|
166
|
-
|
|
167
|
-
when :if
|
|
168
|
-
t = last_expr_type(node.children[1], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
169
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
170
|
-
local_var_types: local_var_types)
|
|
171
|
-
e = last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
172
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
173
|
-
local_var_types: local_var_types)
|
|
174
|
-
unify_types(t, e, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
|
|
175
|
-
|
|
176
|
-
when :case
|
|
177
|
-
branches = node.children[1..].compact.flat_map do |child|
|
|
178
|
-
if child.type == :when
|
|
179
|
-
last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
180
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
181
|
-
local_var_types: local_var_types)
|
|
182
|
-
else
|
|
183
|
-
last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
184
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
185
|
-
local_var_types: local_var_types)
|
|
186
|
-
end
|
|
187
|
-
end.compact
|
|
188
|
-
|
|
189
|
-
if branches.empty?
|
|
190
|
-
fallback_type
|
|
191
|
-
else
|
|
192
|
-
branches.reduce do |a, b|
|
|
193
|
-
unify_types(a, b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
when :return
|
|
198
|
-
Literals.type_from_literal(node.children.first, fallback_type: fallback_type)
|
|
199
|
-
|
|
200
|
-
when :block
|
|
201
|
-
send_node = node.children[0]
|
|
202
|
-
if send_node&.type == :send
|
|
203
|
-
recv = send_node.children[0]
|
|
204
|
-
meth = send_node.children[1]
|
|
205
|
-
|
|
206
|
-
if core_rbs_provider && recv&.type == :lvar
|
|
207
|
-
lvar_name = recv.children.first
|
|
208
|
-
recv_type = nil
|
|
209
|
-
recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
|
|
210
|
-
recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
|
|
211
|
-
if recv_type
|
|
212
|
-
rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
|
|
213
|
-
return rbs_type unless rbs_type == FALLBACK_TYPE
|
|
214
|
-
end
|
|
215
|
-
elsif core_rbs_provider && recv&.type == :send
|
|
216
|
-
inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
|
|
217
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
218
|
-
local_var_types: local_var_types)
|
|
219
|
-
if inner_type
|
|
220
|
-
rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
|
|
221
|
-
return rbs_type unless rbs_type == FALLBACK_TYPE
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
|
|
227
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
228
|
-
local_var_types: local_var_types)
|
|
229
|
-
|
|
230
|
-
when :send
|
|
231
|
-
recv = node.children[0]
|
|
232
|
-
meth = node.children[1]
|
|
233
|
-
|
|
234
|
-
# Try to resolve return type from RBS core for method calls
|
|
235
|
-
if core_rbs_provider && recv&.type == :send
|
|
236
|
-
# Chained call: arg.to_i.positive?
|
|
237
|
-
inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
|
|
238
|
-
core_rbs_provider: core_rbs_provider, param_types: param_types,
|
|
239
|
-
local_var_types: local_var_types)
|
|
240
|
-
if inner_type
|
|
241
|
-
rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
|
|
242
|
-
return rbs_type unless rbs_type == FALLBACK_TYPE
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
elsif core_rbs_provider && recv&.type == :lvar
|
|
246
|
-
# Direct call on local variable: p1.positive? or admins.any?
|
|
247
|
-
lvar_name = recv.children.first
|
|
248
|
-
recv_type = nil
|
|
249
|
-
recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
|
|
250
|
-
recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
|
|
251
|
-
if recv_type
|
|
252
|
-
rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
|
|
253
|
-
return rbs_type unless rbs_type == FALLBACK_TYPE
|
|
254
|
-
end
|
|
255
|
-
end
|
|
402
|
+
def last_expr_type(node, **opts)
|
|
403
|
+
run_last_expr_type(node, **opts)
|
|
404
|
+
end
|
|
256
405
|
|
|
257
|
-
|
|
406
|
+
# Dispatch `last_expr_type` based on node type.
|
|
407
|
+
#
|
|
408
|
+
# @note module_function: when included, also defines #run_last_expr_type (instance visibility: private)
|
|
409
|
+
# @param [Parser::AST::Node, nil] node
|
|
410
|
+
# @param [Hash] opts options passed through as keyword args
|
|
411
|
+
# @return [String, nil]
|
|
412
|
+
def run_last_expr_type(node, **opts)
|
|
413
|
+
return unless node
|
|
258
414
|
|
|
415
|
+
handler = LAST_EXPR_TYPE_HANDLERS[node.type]
|
|
416
|
+
if handler
|
|
417
|
+
send(handler, node, **opts)
|
|
259
418
|
else
|
|
260
|
-
Literals.type_from_literal(node, fallback_type: fallback_type)
|
|
419
|
+
Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
|
|
261
420
|
end
|
|
262
421
|
end
|
|
263
422
|
|
|
264
|
-
#
|
|
423
|
+
# Extract the return type from an explicit `:return` node.
|
|
424
|
+
#
|
|
425
|
+
# @note module_function: when included, also defines #handle_return_node (instance visibility: private)
|
|
426
|
+
# @param [Parser::AST::Node] node the `:return` AST node
|
|
427
|
+
# @param [Hash] opts additional keyword options forwarded to type inference
|
|
428
|
+
# @return [String, nil]
|
|
429
|
+
def handle_return_node(node, **opts)
|
|
430
|
+
Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Resolve an RBS return type for a method call.
|
|
265
434
|
#
|
|
266
435
|
# @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
|
|
267
|
-
# @param [
|
|
268
|
-
# @param [
|
|
269
|
-
# @param [Object] core_rbs_provider
|
|
270
|
-
# @return [
|
|
436
|
+
# @param [String] container_type class or module name
|
|
437
|
+
# @param [String] method_name method name
|
|
438
|
+
# @param [Object] core_rbs_provider core RBS type lookup provider
|
|
439
|
+
# @return [String] inferred return type
|
|
271
440
|
def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
|
|
272
441
|
return FALLBACK_TYPE unless core_rbs_provider
|
|
273
442
|
|
|
@@ -292,14 +461,28 @@ module Docscribe
|
|
|
292
461
|
# @param [String, nil] b
|
|
293
462
|
# @param [String] fallback_type
|
|
294
463
|
# @param [Boolean] nil_as_optional
|
|
464
|
+
# @param [String, nil] type_a first type to unify
|
|
465
|
+
# @param [String, nil] type_b second type to unify
|
|
295
466
|
# @return [String, nil]
|
|
296
|
-
def unify_types(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return
|
|
467
|
+
def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
|
|
468
|
+
type_a ||= fallback_type
|
|
469
|
+
type_b ||= fallback_type
|
|
470
|
+
return type_a if type_a == type_b
|
|
300
471
|
|
|
301
|
-
|
|
302
|
-
|
|
472
|
+
unify_nil_types(type_a, type_b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Unify two types where one may be `nil`, producing optional or union type.
|
|
476
|
+
#
|
|
477
|
+
# @note module_function: when included, also defines #unify_nil_types (instance visibility: private)
|
|
478
|
+
# @param [String] type_a first type string
|
|
479
|
+
# @param [String] type_b second type string
|
|
480
|
+
# @param [String] fallback_type type used when neither is nil
|
|
481
|
+
# @param [Boolean] nil_as_optional whether to render nil unions as optional types
|
|
482
|
+
# @return [String]
|
|
483
|
+
def unify_nil_types(type_a, type_b, fallback_type:, nil_as_optional:)
|
|
484
|
+
if type_a == 'nil' || type_b == 'nil'
|
|
485
|
+
non_nil = (type_a == 'nil' ? type_b : type_a)
|
|
303
486
|
return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
|
|
304
487
|
end
|
|
305
488
|
|
data/lib/docscribe/infer.rb
CHANGED
|
@@ -93,8 +93,8 @@ module Docscribe
|
|
|
93
93
|
# @param [Parser::AST::Node] node
|
|
94
94
|
# @param [String] fallback_type
|
|
95
95
|
# @param [Boolean] nil_as_optional
|
|
96
|
-
# @param [nil] core_rbs_provider
|
|
97
|
-
# @param [nil] param_types
|
|
96
|
+
# @param [nil] core_rbs_provider core RBS type lookup provider
|
|
97
|
+
# @param [nil] param_types parameter name -> type map
|
|
98
98
|
# @return [Hash]
|
|
99
99
|
def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
|
|
100
100
|
param_types: nil)
|
|
@@ -124,9 +124,10 @@ module Docscribe
|
|
|
124
124
|
# Convert a constant AST node into its fully qualified name.
|
|
125
125
|
#
|
|
126
126
|
# @param [Parser::AST::Node, nil] n
|
|
127
|
+
# @param [Parser::AST::Node, nil] node constant AST node to resolve
|
|
127
128
|
# @return [String, nil]
|
|
128
|
-
def const_full_name(
|
|
129
|
-
Names.const_full_name(
|
|
129
|
+
def const_full_name(node)
|
|
130
|
+
Names.const_full_name(node)
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
# Infer a YARD-ish type string from a literal AST node.
|
|
@@ -140,15 +141,15 @@ module Docscribe
|
|
|
140
141
|
|
|
141
142
|
# Unify two inferred type strings conservatively.
|
|
142
143
|
#
|
|
143
|
-
# @param [String, nil]
|
|
144
|
-
# @param [String, nil]
|
|
144
|
+
# @param [String, nil] type_a
|
|
145
|
+
# @param [String, nil] type_b
|
|
145
146
|
# @param [String] fallback_type
|
|
146
147
|
# @param [Boolean] nil_as_optional
|
|
147
148
|
# @return [String]
|
|
148
|
-
def unify_types(
|
|
149
|
+
def unify_types(type_a, type_b, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
|
|
149
150
|
Returns.unify_types(
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
type_a,
|
|
152
|
+
type_b,
|
|
152
153
|
fallback_type: fallback_type,
|
|
153
154
|
nil_as_optional: nil_as_optional
|
|
154
155
|
)
|