docscribe 1.4.2 → 1.5.1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +601 -139
  3. data/exe/docscribe-client +105 -0
  4. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  5. data/lib/docscribe/cli/config_builder.rb +107 -53
  6. data/lib/docscribe/cli/formatters/json.rb +294 -0
  7. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  8. data/lib/docscribe/cli/formatters/text.rb +208 -0
  9. data/lib/docscribe/cli/formatters.rb +26 -0
  10. data/lib/docscribe/cli/generate.rb +56 -62
  11. data/lib/docscribe/cli/init.rb +14 -6
  12. data/lib/docscribe/cli/options.rb +206 -89
  13. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  14. data/lib/docscribe/cli/run.rb +433 -154
  15. data/lib/docscribe/cli/server.rb +135 -0
  16. data/lib/docscribe/cli/sigs.rb +366 -0
  17. data/lib/docscribe/cli/update_types.rb +103 -0
  18. data/lib/docscribe/cli.rb +21 -24
  19. data/lib/docscribe/config/defaults.rb +7 -2
  20. data/lib/docscribe/config/emit.rb +17 -0
  21. data/lib/docscribe/config/filtering.rb +17 -24
  22. data/lib/docscribe/config/loader.rb +19 -17
  23. data/lib/docscribe/config/plugin.rb +1 -1
  24. data/lib/docscribe/config/rbs.rb +39 -7
  25. data/lib/docscribe/config/sorbet.rb +22 -16
  26. data/lib/docscribe/config/sorting.rb +1 -1
  27. data/lib/docscribe/config/template.rb +10 -1
  28. data/lib/docscribe/config/utils.rb +11 -9
  29. data/lib/docscribe/config.rb +10 -6
  30. data/lib/docscribe/infer/ast_walk.rb +1 -1
  31. data/lib/docscribe/infer/literals.rb +6 -11
  32. data/lib/docscribe/infer/names.rb +2 -3
  33. data/lib/docscribe/infer/params.rb +14 -16
  34. data/lib/docscribe/infer/raises.rb +3 -5
  35. data/lib/docscribe/infer/returns.rb +615 -151
  36. data/lib/docscribe/infer.rb +29 -26
  37. data/lib/docscribe/inline_rewriter/collector.rb +159 -164
  38. data/lib/docscribe/inline_rewriter/doc_block.rb +145 -115
  39. data/lib/docscribe/inline_rewriter/doc_builder.rb +1032 -723
  40. data/lib/docscribe/inline_rewriter/source_helpers.rb +48 -48
  41. data/lib/docscribe/inline_rewriter/tag_sorter.rb +82 -85
  42. data/lib/docscribe/inline_rewriter.rb +485 -488
  43. data/lib/docscribe/lru_cache.rb +49 -0
  44. data/lib/docscribe/parsing.rb +28 -9
  45. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -1
  46. data/lib/docscribe/plugin/base/tag_plugin.rb +0 -1
  47. data/lib/docscribe/plugin/context.rb +28 -18
  48. data/lib/docscribe/plugin/registry.rb +25 -26
  49. data/lib/docscribe/plugin/tag.rb +9 -14
  50. data/lib/docscribe/plugin.rb +17 -16
  51. data/lib/docscribe/server.rb +608 -0
  52. data/lib/docscribe/types/provider_chain.rb +4 -2
  53. data/lib/docscribe/types/rbs/collection_loader.rb +2 -2
  54. data/lib/docscribe/types/rbs/provider.rb +177 -51
  55. data/lib/docscribe/types/rbs/type_formatter.rb +224 -83
  56. data/lib/docscribe/types/signature.rb +22 -42
  57. data/lib/docscribe/types/sorbet/base_provider.rb +29 -21
  58. data/lib/docscribe/types/sorbet/rbi_provider.rb +6 -5
  59. data/lib/docscribe/types/sorbet/source_provider.rb +6 -4
  60. data/lib/docscribe/types/yard/formatter.rb +100 -0
  61. data/lib/docscribe/types/yard/parser.rb +240 -0
  62. data/lib/docscribe/types/yard/types.rb +52 -0
  63. data/lib/docscribe/version.rb +1 -1
  64. metadata +38 -1
@@ -6,24 +6,16 @@ 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
-
18
9
  # Infer a return type from a full method definition source string.
19
10
  #
20
11
  # The source must parse to a `:def` or `:defs` node. If parsing fails or inference
21
12
  # is uncertain, the fallback type is returned.
22
13
  #
23
- # @note module_function: when included, also defines #infer_return_type (instance visibility: private)
24
- # @param [String, nil] method_source full method definition source
14
+ # @note module_function: defines #infer_return_type (visibility: private)
15
+ # @param [String?] method_source full method definition source
25
16
  # @raise [Parser::SyntaxError]
26
17
  # @return [String]
18
+ # @return [FALLBACK_TYPE] if Parser::SyntaxError
27
19
  def infer_return_type(method_source)
28
20
  return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
29
21
 
@@ -34,13 +26,13 @@ module Docscribe
34
26
  local_var_types = build_local_variable_types(body)
35
27
  run_last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
36
28
  local_var_types: local_var_types) || FALLBACK_TYPE
37
- rescue Parser::SyntaxError
29
+ rescue Parser::SyntaxError # steep:ignore
38
30
  FALLBACK_TYPE
39
31
  end
40
32
 
41
33
  # Parse a Ruby source string into an AST using the Parser gem.
42
34
  #
43
- # @note module_function: when included, also defines #parse_method_source (instance visibility: private)
35
+ # @note module_function: defines #parse_method_source (visibility: private)
44
36
  # @param [String] method_source the method definition source string to parse
45
37
  # @return [Parser::AST::Node, nil]
46
38
  def parse_method_source(method_source)
@@ -51,7 +43,7 @@ module Docscribe
51
43
 
52
44
  # Infer a method's normal return type from an already parsed def/defs node.
53
45
  #
54
- # @note module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
46
+ # @note module_function: defines #infer_return_type_from_node (visibility: private)
55
47
  # @param [Parser::AST::Node] node `:def` or `:defs` node
56
48
  # @return [String]
57
49
  def infer_return_type_from_node(node)
@@ -69,33 +61,31 @@ module Docscribe
69
61
  # - `:normal` => normal/happy-path return type
70
62
  # - `:rescues` => array of `[exception_names, return_type]` pairs for rescue branches
71
63
  #
72
- # @note module_function: when included, also defines #returns_spec_from_node (instance visibility: private)
64
+ # @note module_function: defines #returns_spec_from_node (visibility: private)
73
65
  # @param [Parser::AST::Node] node `:def` or `:defs` node
74
66
  # @param [String] fallback_type type used when inference is uncertain
75
67
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
76
- # @param [nil] core_rbs_provider core RBS type lookup provider
77
- # @param [nil] param_types parameter name -> type map
78
- # @return [Hash]
79
- def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
80
- param_types: nil)
68
+ # @param [Object?] core_rbs_provider core RBS type lookup provider
69
+ # @param [Hash<String, String>?] param_types parameter name -> type map
70
+ # @param [String?] container
71
+ # @param [Docscribe::Types::ProviderChain?] signature_provider
72
+ # @return [Object]
73
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, # rubocop:disable Metrics/ParameterLists
74
+ param_types: nil, container: nil, signature_provider: nil)
81
75
  body = extract_def_body(node)
82
76
  spec = { normal: FALLBACK_TYPE, rescues: [] } #: Hash[Symbol, untyped]
83
77
  return spec unless body
84
78
 
85
- local_var_types = build_local_variable_types(body)
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
-
79
+ types = build_local_variable_types(body, core_rbs_provider: core_rbs_provider, param_types: param_types)
80
+ populate_returns_spec(spec, body, types, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
81
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
82
+ container: container, signature_provider: signature_provider)
93
83
  spec
94
84
  end
95
85
 
96
86
  # Extract the body child node from a `:def` or `:defs` AST node.
97
87
  #
98
- # @note module_function: when included, also defines #extract_def_body (instance visibility: private)
88
+ # @note module_function: defines #extract_def_body (visibility: private)
99
89
  # @param [Parser::AST::Node] node a `:def` or `:defs` AST node
100
90
  # @return [Parser::AST::Node, nil]
101
91
  def extract_def_body(node)
@@ -107,12 +97,12 @@ module Docscribe
107
97
 
108
98
  # Populate the spec hash with normal and/or rescue return types from the body.
109
99
  #
110
- # @note module_function: when included, also defines #populate_returns_spec (instance visibility: private)
111
- # @param [Hash] spec the return spec hash to populate
100
+ # @note module_function: defines #populate_returns_spec (visibility: private)
101
+ # @param [Object] spec the return spec hash to populate
112
102
  # @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]
103
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
104
+ # @param [Object] opts additional keyword options forwarded to type inference
105
+ # @return [Object]
116
106
  def populate_returns_spec(spec, body, local_var_types, **opts)
117
107
  if body.type == :rescue
118
108
  process_rescue_body(spec, body, **opts)
@@ -123,9 +113,9 @@ module Docscribe
123
113
 
124
114
  # Infer the normal (non-rescue) return type from a method body node.
125
115
  #
126
- # @note module_function: when included, also defines #infer_normal_return_type (instance visibility: private)
116
+ # @note module_function: defines #infer_normal_return_type (visibility: private)
127
117
  # @param [Parser::AST::Node] body the method body AST node
128
- # @param [Hash] opts additional keyword options forwarded to type inference
118
+ # @param [Object] opts additional keyword options forwarded to type inference
129
119
  # @return [String]
130
120
  def infer_normal_return_type(body, **opts)
131
121
  run_last_expr_type(body, **opts) || FALLBACK_TYPE
@@ -133,18 +123,16 @@ module Docscribe
133
123
 
134
124
  # Process a :rescue body node and populate spec with normal + rescue return types.
135
125
  #
136
- # @note module_function: when included, also defines #process_rescue_body (instance visibility: private)
137
- # @param [Hash] spec the return spec hash to populate
126
+ # @note module_function: defines #process_rescue_body (visibility: private)
127
+ # @param [Object] spec the return spec hash to populate
138
128
  # @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]
129
+ # @param [Object] opts additional keyword options forwarded to type inference
130
+ # @return [Object]
145
131
  def process_rescue_body(spec, body, **opts)
146
132
  main_body = body.children[0]
147
- local_var_types = build_local_variable_types(body)
133
+ local_var_types = build_local_variable_types(body,
134
+ core_rbs_provider: opts[:core_rbs_provider],
135
+ param_types: opts[:param_types])
148
136
  rescue_opts = opts.merge(local_var_types: local_var_types)
149
137
  spec[:normal] = run_last_expr_type(main_body, **rescue_opts) || FALLBACK_TYPE
150
138
  process_rescue_branches(spec, body, **rescue_opts)
@@ -152,11 +140,11 @@ module Docscribe
152
140
 
153
141
  # Extract return types from each :resbody child and append to spec[:rescues].
154
142
  #
155
- # @note module_function: when included, also defines #process_rescue_branches (instance visibility: private)
156
- # @param [Hash] spec the return spec hash to populate
143
+ # @note module_function: defines #process_rescue_branches (visibility: private)
144
+ # @param [Object] spec the return spec hash to populate
157
145
  # @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
146
+ # @param [Object] opts additional keyword options forwarded to type inference
147
+ # @return [Array<Object>] the list of rescue type entries
160
148
  def process_rescue_branches(spec, body, **opts)
161
149
  body.children.each do |ch|
162
150
  next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
@@ -170,76 +158,229 @@ module Docscribe
170
158
 
171
159
  # Build a map of local/global/ivar/constant assignments to inferred types.
172
160
  #
173
- # @note module_function: when included, also defines #build_local_variable_types (instance visibility: private)
161
+ # @note module_function: defines #build_local_variable_types (visibility: private)
174
162
  # @param [Parser::AST::Node] node AST node to walk
175
- # @return [Hash, nil]
176
- def build_local_variable_types(node)
163
+ # @param [Object] opts additional keyword options forwarded to inference
164
+ # @return [Hash<String, String>, nil]
165
+ def build_local_variable_types(node, **opts)
177
166
  types = {} #: Hash[String, String]
178
167
  ASTWalk.walk(node) do |n|
179
- collect_assignment_type(n, types)
168
+ collect_assignment_type(n, types, **opts)
180
169
  end
181
170
  types.empty? ? nil : types
182
171
  end
183
172
 
184
173
  # Infer the type of a single assignment node and store it in the types hash.
185
174
  #
186
- # @note module_function: when included, also defines #collect_assignment_type (instance visibility: private)
175
+ # Uses `run_last_expr_type` when `core_rbs_provider` is available to
176
+ # resolve send expressions (e.g., `x = 123 + 1` -> `Integer`).
177
+ # Falls back to `Literals.type_from_literal` for plain literals.
178
+ #
179
+ # @note module_function: defines #collect_assignment_type (visibility: private)
187
180
  # @param [Parser::AST::Node] node an assignment AST node
188
- # @param [Hash] types the accumulated local variable type map
181
+ # @param [Hash<String, String>] types the accumulated local variable type map
182
+ # @param [Object] opts additional keyword options forwarded to inference
189
183
  # @return [void]
190
- def collect_assignment_type(node, types)
184
+ def collect_assignment_type(node, types, **opts)
191
185
  name, value = assignment_name_and_value(node)
192
186
  return unless name && value
193
187
 
194
- inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
188
+ inferred = if opts[:core_rbs_provider]
189
+ run_last_expr_type(value, **opts, fallback_type: FALLBACK_TYPE,
190
+ nil_as_optional: false, local_var_types: types)
191
+ else
192
+ Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
193
+ end
195
194
  types[name] = inferred if inferred && inferred != FALLBACK_TYPE
196
195
  end
197
196
 
198
197
  # Extract the variable name and value expression from an assignment node.
199
198
  #
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
199
+ # @note module_function: defines #assignment_name_and_value (visibility: private)
200
+ # @param [Parser::AST::Node] node an assignment AST node (:lvasgn, :gvasgn, :ivasgn, :casgn, :op_asgn)
201
+ # @return [(String, nil, Parser::AST::Node, nil)]
203
202
  def assignment_name_and_value(node)
204
203
  case node.type
205
- when :lvasgn, :gvasgn, :ivasgn
204
+ when :lvasgn, :gvasgn, :ivasgn, :cvasgn
206
205
  [node.children[0].to_s, node.children[1]]
207
206
  when :casgn
208
- [node.children[0].to_s, node.children[2]]
207
+ constant_name_and_value(node)
208
+ when :op_asgn
209
+ compound_name_and_value(node)
209
210
  else
210
211
  [nil, nil]
211
212
  end
212
213
  end
213
214
 
215
+ # Extract the name and value from a `:casgn` (constant assignment) node.
216
+ #
217
+ # @note module_function: defines #constant_name_and_value (visibility: private)
218
+ # @param [Parser::AST::Node] node the `:casgn` AST node
219
+ # @return [(String, nil, Parser::AST::Node, nil)]
220
+ def constant_name_and_value(node)
221
+ [node.children[0].to_s, node.children[2]]
222
+ end
223
+
224
+ # Extract the name and value from an `:op_asgn` (compound assignment) node.
225
+ #
226
+ # @note module_function: defines #compound_name_and_value (visibility: private)
227
+ # @param [Parser::AST::Node] node the `:op_asgn` AST node
228
+ # @return [(String, nil, Parser::AST::Node, nil)]
229
+ def compound_name_and_value(node)
230
+ [node.children[0].children.first.to_s, node.children[2]]
231
+ end
232
+
233
+ # Handle `:lvar` node for last_expr_type — look up the variable in local_var_types.
234
+ #
235
+ # @note module_function: defines #handle_lvar_node (visibility: private)
236
+ # @param [Parser::AST::Node] node the `:lvar` AST node
237
+ # @param [Object] opts additional keyword options forwarded to type inference
238
+ # @return [String, nil]
239
+ def handle_lvar_node(node, **opts)
240
+ name = node.children[0].to_s
241
+ lookup_lvar_type(name, opts[:local_var_types], opts[:param_types]) || opts[:fallback_type]
242
+ end
243
+
244
+ # Handle `:ivar` node for last_expr_type — look up instance variable in local_var_types.
245
+ #
246
+ # @note module_function: defines #handle_ivar_node (visibility: private)
247
+ # @param [Parser::AST::Node] node the `:ivar` AST node
248
+ # @param [Object] opts additional keyword options forwarded to type inference
249
+ # @return [String, nil]
250
+ def handle_ivar_node(node, **opts)
251
+ name = node.children[0].to_s
252
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
253
+ end
254
+
255
+ # Handle `:gvar` node for last_expr_type — look up global variable in local_var_types.
256
+ #
257
+ # @note module_function: defines #handle_gvar_node (visibility: private)
258
+ # @param [Parser::AST::Node] node the `:gvar` AST node
259
+ # @param [Object] opts additional keyword options forwarded to type inference
260
+ # @return [String, nil]
261
+ def handle_gvar_node(node, **opts)
262
+ name = node.children[0].to_s
263
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
264
+ end
265
+
266
+ # Handle `:cvar` node for last_expr_type — look up class variable in local_var_types.
267
+ #
268
+ # @note module_function: defines #handle_cvar_node (visibility: private)
269
+ # @param [Parser::AST::Node] node the `:cvar` AST node
270
+ # @param [Object] opts additional keyword options forwarded to type inference
271
+ # @return [String, nil]
272
+ def handle_cvar_node(node, **opts)
273
+ name = node.children[0].to_s
274
+ opts[:local_var_types]&.fetch(name, nil) || opts[:fallback_type]
275
+ end
276
+
277
+ # Handle `:lvasgn` node for last_expr_type — look up local var assignment in local_var_types.
278
+ #
279
+ # @note module_function: defines #handle_lvasgn_node (visibility: private)
280
+ # @param [Parser::AST::Node] node the `:lvasgn` AST node
281
+ # @param [Object] opts additional keyword options forwarded to type inference
282
+ # @return [String, nil]
283
+ def handle_lvasgn_node(node, **opts)
284
+ name = node.children[0].to_s
285
+ opts[:local_var_types]&.fetch(name, nil) ||
286
+ run_last_expr_type(node.children[1], **opts) ||
287
+ opts[:fallback_type]
288
+ end
289
+
290
+ # Handle `:ivasgn` node for last_expr_type — look up ivar assignment in local_var_types.
291
+ #
292
+ # @note module_function: defines #handle_ivasgn_node (visibility: private)
293
+ # @param [Parser::AST::Node] node the `:ivasgn` AST node
294
+ # @param [Object] opts additional keyword options forwarded to type inference
295
+ # @return [String, nil]
296
+ def handle_ivasgn_node(node, **opts)
297
+ name = node.children[0].to_s
298
+ opts[:local_var_types]&.fetch(name, nil) ||
299
+ run_last_expr_type(node.children[1], **opts) ||
300
+ opts[:fallback_type]
301
+ end
302
+
303
+ # Handle `:gvasgn` node for last_expr_type — look up global var assignment in local_var_types.
304
+ #
305
+ # @note module_function: defines #handle_gvasgn_node (visibility: private)
306
+ # @param [Parser::AST::Node] node the `:gvasgn` AST node
307
+ # @param [Object] opts additional keyword options forwarded to type inference
308
+ # @return [String, nil]
309
+ def handle_gvasgn_node(node, **opts)
310
+ name = node.children[0].to_s
311
+ opts[:local_var_types]&.fetch(name, nil) ||
312
+ run_last_expr_type(node.children[1], **opts) ||
313
+ opts[:fallback_type]
314
+ end
315
+
316
+ # Handle `:cvasgn` node for last_expr_type — look up class var assignment in local_var_types.
317
+ #
318
+ # @note module_function: defines #handle_cvasgn_node (visibility: private)
319
+ # @param [Parser::AST::Node] node the `:cvasgn` AST node
320
+ # @param [Object] opts additional keyword options forwarded to type inference
321
+ # @return [String, nil]
322
+ def handle_cvasgn_node(node, **opts)
323
+ name = node.children[0].to_s
324
+ opts[:local_var_types]&.fetch(name, nil) ||
325
+ run_last_expr_type(node.children[1], **opts) ||
326
+ opts[:fallback_type]
327
+ end
328
+
329
+ # Handle `:op_asgn` node (compound assignment: `x += 1`, `@var -= 2`, etc.).
330
+ #
331
+ # Infers the result type from the operator and the right operand's type.
332
+ # Uses RBS to resolve when available (e.g., `Integer#+` -> `Integer`).
333
+ #
334
+ # @note module_function: defines #handle_op_asgn_node (visibility: private)
335
+ # @param [Parser::AST::Node] node the `:op_asgn` AST node
336
+ # @param [Object] opts additional keyword options forwarded to type inference
337
+ # @return [String, nil]
338
+ def handle_op_asgn_node(node, **opts)
339
+ meth = node.children[1]
340
+ return nil unless %i[+ - * / % ** << | & ^].include?(meth)
341
+ return nil unless opts[:core_rbs_provider]
342
+
343
+ arg = node.children[2]
344
+ arg_type = type_from_literal_safe(arg)
345
+ return nil unless arg_type
346
+
347
+ rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
348
+ rbs unless rbs == FALLBACK_TYPE
349
+ end
350
+
214
351
  # Handle `:begin` node for last_expr_type.
215
352
  #
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]
353
+ # @note module_function: defines #handle_begin_node (visibility: private)
354
+ # @param [Parser::AST::Node] node the `:return` AST node
355
+ # @param [Object] opts additional keyword options forwarded to type inference
356
+ # @return [String, nil]
220
357
  def handle_begin_node(node, **opts)
221
358
  run_last_expr_type(node.children.last, **opts)
222
359
  end
223
360
 
224
361
  # Handle `:if` node for last_expr_type.
225
362
  #
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]
363
+ # @note module_function: defines #handle_if_node (visibility: private)
364
+ # @param [Parser::AST::Node] node the `:return` AST node
365
+ # @param [Object] opts additional keyword options forwarded to type inference
366
+ # @return [String, nil]
230
367
  def handle_if_node(node, **opts)
231
368
  t = run_last_expr_type(node.children[1], **opts)
232
- e = run_last_expr_type(node.children[2], **opts)
369
+ e = if node.children[2]
370
+ run_last_expr_type(node.children[2], **opts)
371
+ else
372
+ 'nil'
373
+ end
233
374
  unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
234
375
  nil_as_optional: opts.fetch(:nil_as_optional, true))
235
376
  end
236
377
 
237
378
  # Handle `:case` node for last_expr_type.
238
379
  #
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]
380
+ # @note module_function: defines #handle_case_node (visibility: private)
381
+ # @param [Parser::AST::Node] node the `:return` AST node
382
+ # @param [Object] opts additional keyword options forwarded to type inference
383
+ # @return [String, nil]
243
384
  def handle_case_node(node, **opts)
244
385
  branches = process_case_branches(node, **opts)
245
386
  if branches.empty?
@@ -252,11 +393,204 @@ module Docscribe
252
393
  end
253
394
  end
254
395
 
396
+ # Handle `:or` node (`a || b`) for last_expr_type.
397
+ #
398
+ # The result type is the union of both sides, since either may be returned
399
+ # depending on the truthiness of the left operand.
400
+ #
401
+ # @note module_function: defines #handle_or_node (visibility: private)
402
+ # @param [Parser::AST::Node] node the `:or` AST node
403
+ # @param [Object] opts additional keyword options forwarded to type inference
404
+ # @return [String, nil]
405
+ def handle_or_node(node, **opts)
406
+ t = run_last_expr_type(node.children[0], **opts)
407
+ e = run_last_expr_type(node.children[1], **opts)
408
+ unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
409
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
410
+ end
411
+
412
+ # Handle `:and` node (`a && b`) for last_expr_type.
413
+ #
414
+ # The result type is the union of both sides, since either may be returned
415
+ # depending on the truthiness of the left operand.
416
+ #
417
+ # @note module_function: defines #handle_and_node (visibility: private)
418
+ # @param [Parser::AST::Node] node the `:and` AST node
419
+ # @param [Object] opts additional keyword options forwarded to type inference
420
+ # @return [String, nil]
421
+ def handle_and_node(node, **opts)
422
+ t = run_last_expr_type(node.children[0], **opts)
423
+ e = run_last_expr_type(node.children[1], **opts)
424
+ unify_types(t, e, fallback_type: opts[:fallback_type] || 'untyped',
425
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
426
+ end
427
+
428
+ # Handle `:kwbegin` node (`begin; expr; end`) for last_expr_type.
429
+ #
430
+ # Unwraps the explicit begin node and delegates to the inner expression,
431
+ # which may be a `:rescue` or `:ensure` node.
432
+ #
433
+ # @note module_function: defines #handle_kwbegin_node (visibility: private)
434
+ # @param [Parser::AST::Node] node the `:kwbegin` AST node
435
+ # @param [Object] opts additional keyword options forwarded to type inference
436
+ # @return [String, nil]
437
+ def handle_kwbegin_node(node, **opts)
438
+ run_last_expr_type(node.children.first, **opts)
439
+ end
440
+
441
+ # Handle `:rescue` node for last_expr_type.
442
+ #
443
+ # Supports both inline rescue (`expr rescue default`) and block rescue
444
+ # (`begin; expr; rescue; e; end`).
445
+ #
446
+ # @note module_function: defines #handle_rescue_node (visibility: private)
447
+ # @param [Parser::AST::Node] node the `:rescue` AST node
448
+ # @param [Object] opts additional keyword options forwarded to type inference
449
+ # @return [String, nil]
450
+ def handle_rescue_node(node, **opts)
451
+ branches = collect_rescue_branches(node, **opts)
452
+ branches.reduce do |a, b|
453
+ unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
454
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
455
+ end
456
+ end
457
+
458
+ # Handle `:rescue` node for last_expr_type.
459
+ #
460
+ # Unifies the body type with all rescue handler types and the optional else clause.
461
+ # Collect all rescue branch return types from a `:rescue` AST node.
462
+ #
463
+ # @note module_function: defines #collect_rescue_branches (visibility: private)
464
+ # @param [Parser::AST::Node] node the `:rescue` AST node
465
+ # @param [Object] opts additional keyword options forwarded to type inference
466
+ # @return [Array<String, nil>]
467
+ def collect_rescue_branches(node, **opts)
468
+ branches = [run_last_expr_type(node.children[0], **opts)]
469
+ (node.children[1..] || []).each do |child|
470
+ if child.is_a?(Parser::AST::Node) && child.type == :resbody
471
+ handler = child.children[2]
472
+ branches << run_last_expr_type(handler, **opts) if handler
473
+ else
474
+ branches << run_last_expr_type(child, **opts)
475
+ end
476
+ end
477
+ branches
478
+ end
479
+
480
+ # Handle `:ensure` node (`begin; expr; ensure; cleanup; end`) for last_expr_type.
481
+ #
482
+ # The ensure clause's result is discarded by Ruby; only the body type is returned.
483
+ #
484
+ # @note module_function: defines #handle_ensure_node (visibility: private)
485
+ # @param [Parser::AST::Node] node the `:ensure` AST node
486
+ # @param [Object] opts additional keyword options forwarded to type inference
487
+ # @return [String, nil]
488
+ def handle_ensure_node(node, **opts)
489
+ run_last_expr_type(node.children[0], **opts)
490
+ end
491
+
492
+ # Handle `:defined?` node (`defined?(expr)`) for last_expr_type.
493
+ #
494
+ # Returns `nil` if the expression is not defined, or a String description
495
+ # if it is defined. The union type is `String?`.
496
+ #
497
+ # @note module_function: defines #handle_defined_node (visibility: private)
498
+ # @param [Parser::AST::Node] _node the `:defined?` AST node
499
+ # @param [Object] opts additional keyword options forwarded to type inference
500
+ # @return [String, nil]
501
+ def handle_defined_node(_node, **opts)
502
+ nil_as_optional = opts.fetch(:nil_as_optional, true)
503
+ nil_as_optional ? 'String?' : 'String, nil'
504
+ end
505
+
506
+ # Handle `:zsuper` node (`super` with no arguments) for last_expr_type.
507
+ #
508
+ # Returns the super method's return type if resolvable via RBS, or the
509
+ # fallback type otherwise.
510
+ #
511
+ # @note module_function: defines #handle_zsuper_node (visibility: private)
512
+ # @param [Parser::AST::Node] _node the `:zsuper` AST node
513
+ # @param [Object] opts additional keyword options forwarded to type inference
514
+ # @return [String, nil]
515
+ def handle_zsuper_node(_node, **opts)
516
+ opts[:fallback_type]
517
+ end
518
+
519
+ # Handle `:super` node (`super(args)`) for last_expr_type.
520
+ #
521
+ # Returns the super method's return type if resolvable via RBS, or the
522
+ # fallback type otherwise.
523
+ #
524
+ # @note module_function: defines #handle_super_node (visibility: private)
525
+ # @param [Parser::AST::Node] _node the `:super` AST node
526
+ # @param [Object] opts additional keyword options forwarded to type inference
527
+ # @return [String, nil]
528
+ def handle_super_node(_node, **opts)
529
+ opts[:fallback_type]
530
+ end
531
+
532
+ # Handle `:yield` node (`yield` / `yield(args)`) for last_expr_type.
533
+ #
534
+ # Returns the block's return type if resolvable via RBS (`Proc#call`),
535
+ # or the fallback type otherwise.
536
+ #
537
+ # @note module_function: defines #handle_yield_node (visibility: private)
538
+ # @param [Parser::AST::Node] _node the `:yield` AST node
539
+ # @param [Object] opts additional keyword options forwarded to type inference
540
+ # @return [String, nil]
541
+ def handle_yield_node(_node, **opts)
542
+ opts[:fallback_type]
543
+ end
544
+
545
+ # Handle `:case_match` node (`case x; in pat; expr; end`) for last_expr_type.
546
+ #
547
+ # Similar to `:case` — unifies all `in_pattern` branch types and the optional else clause.
548
+ #
549
+ # @note module_function: defines #handle_case_match_node (visibility: private)
550
+ # @param [Parser::AST::Node] node the `:case_match` AST node
551
+ # @param [Object] opts additional keyword options forwarded to type inference
552
+ # @return [String, nil]
553
+ def handle_case_match_node(node, **opts)
554
+ branches = process_pattern_branches(node, **opts)
555
+ if branches.empty?
556
+ opts[:fallback_type]
557
+ else
558
+ branches.reduce do |a, b|
559
+ unify_types(a, b, fallback_type: opts[:fallback_type] || 'untyped',
560
+ nil_as_optional: opts.fetch(:nil_as_optional, true))
561
+ end
562
+ end
563
+ end
564
+
565
+ # Handle `:in_pattern` node (pattern inside `case...in`) for last_expr_type.
566
+ #
567
+ # Extracts the body expression from the pattern and recurses.
568
+ #
569
+ # @note module_function: defines #handle_in_pattern_node (visibility: private)
570
+ # @param [Parser::AST::Node] node the `:in_pattern` AST node
571
+ # @param [Object] opts additional keyword options forwarded to type inference
572
+ # @return [String, nil]
573
+ def handle_in_pattern_node(node, **opts)
574
+ run_last_expr_type(node.children[2], **opts)
575
+ end
576
+
577
+ # Extract inferred return types from all in_pattern branches of a :case_match expression.
578
+ #
579
+ # @note module_function: defines #process_pattern_branches (visibility: private)
580
+ # @param [Parser::AST::Node] node the :case_match AST node
581
+ # @param [Object] opts additional keyword options forwarded to type inference
582
+ # @return [Array<String>] list of inferred types from each branch
583
+ def process_pattern_branches(node, **opts)
584
+ (node.children[1..] || []).compact.filter_map do |child|
585
+ run_last_expr_type(child, **opts) if child.is_a?(Parser::AST::Node)
586
+ end
587
+ end
588
+
255
589
  # Extract inferred return types from all branches of a :case expression.
256
590
  #
257
- # @note module_function: when included, also defines #process_case_branches (instance visibility: private)
591
+ # @note module_function: defines #process_case_branches (visibility: private)
258
592
  # @param [Parser::AST::Node] node the :case AST node
259
- # @param [Hash] opts additional keyword options forwarded to type inference
593
+ # @param [Object] opts additional keyword options forwarded to type inference
260
594
  # @return [Array<String>] list of inferred types from each branch
261
595
  def process_case_branches(node, **opts)
262
596
  (node.children[1..] || []).compact.flat_map do |child|
@@ -270,18 +604,15 @@ module Docscribe
270
604
 
271
605
  # Handle `:block` node for last_expr_type.
272
606
  #
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]
607
+ # @note module_function: defines #handle_block_node (visibility: private)
608
+ # @param [Parser::AST::Node] node the `:return` AST node
609
+ # @param [Object] opts additional keyword options forwarded to type inference
610
+ # @return [String, nil]
277
611
  def handle_block_node(node, **opts)
278
612
  send_node = node.children[0]
279
613
  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
614
+ type = send_rbs_type(send_node.children[0], send_node.children[1], **opts)
615
+ return type if type
285
616
  end
286
617
 
287
618
  run_last_expr_type(node.children[2], **opts)
@@ -289,53 +620,194 @@ module Docscribe
289
620
 
290
621
  # Handle `:send` node for last_expr_type.
291
622
  #
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]
623
+ # @note module_function: defines #handle_send_node (visibility: private)
624
+ # @param [Parser::AST::Node] node the `:return` AST node
625
+ # @param [Object] opts additional keyword options forwarded to type inference
626
+ # @return [String, nil]
296
627
  def handle_send_node(node, **opts)
297
628
  recv = node.children[0]
298
629
  meth = node.children[1]
299
630
 
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
631
+ rbs_type = send_rbs_type(recv, meth, **opts) if opts[:core_rbs_provider]
632
+ return rbs_type if rbs_type
633
+
634
+ compound_type = infer_from_compound_assign(node, **opts)
635
+ return compound_type if compound_type
305
636
 
306
637
  Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
307
638
  end
308
639
 
640
+ # Resolve RBS return type for a send node, trying explicit receiver first,
641
+ # then falling back to the method's container for implicit self calls.
642
+ #
643
+ # @note module_function: defines #send_rbs_type (visibility: private)
644
+ # @param [Parser::AST::Node, nil] recv the receiver node
645
+ # @param [Symbol] meth the method name
646
+ # @param [Object] opts additional keyword options
647
+ # @return [String, nil]
648
+ def send_rbs_type(recv, meth, **opts)
649
+ rbs_type = resolve_rbs_for_send(recv, meth, opts[:core_rbs_provider], opts[:local_var_types],
650
+ opts[:param_types])
651
+ return rbs_type if rbs_type
652
+
653
+ rbs_type = resolve_rbs_for_send_with_signature_provider(recv, meth, **opts)
654
+ return rbs_type if rbs_type
655
+
656
+ container_rbs_return_type(meth, **opts) if recv.nil?
657
+ end
658
+
309
659
  # Resolve RBS return type for a send node's receiver, if possible.
310
660
  #
311
- # Handles `:lvar` and chained `:send` receivers.
661
+ # Handles `:lvar`, chained `:send`, literal (`:int`, `:str`, etc.),
662
+ # and variable (`:ivar`, `:gvar`, `:cvar`) receivers.
312
663
  #
313
- # @note module_function: when included, also defines #resolve_rbs_for_send (instance visibility: private)
664
+ # @note module_function: defines #resolve_rbs_for_send (visibility: private)
314
665
  # @param [Parser::AST::Node, nil] recv the receiver node of the send
315
666
  # @param [Symbol] meth the method name being called
316
667
  # @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
668
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
669
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
319
670
  # @return [String, nil] resolved type or nil if unresolvable
320
671
  def resolve_rbs_for_send(recv, meth, core_rbs_provider, local_var_types, param_types)
321
- return nil unless core_rbs_provider
672
+ recv_type = receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
673
+ return nil unless recv_type
322
674
 
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)
675
+ if core_rbs_provider
676
+ rbs = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
677
+ return rbs unless rbs == FALLBACK_TYPE
327
678
  end
679
+
680
+ nil
681
+ end
682
+
683
+ # Resolve RBS return type via signature_provider fallback.
684
+ #
685
+ # Tries project-level RBS when core_rbs_provider fails.
686
+ #
687
+ # @note module_function: defines #resolve_rbs_for_send_with_signature_provider (visibility: private)
688
+ # @param [Parser::AST::Node, nil] recv the receiver node
689
+ # @param [Symbol] meth the method name
690
+ # @param [Object] opts additional keyword options
691
+ # @return [String, nil]
692
+ def resolve_rbs_for_send_with_signature_provider(recv, meth, **opts)
693
+ return nil unless opts[:signature_provider]
694
+
695
+ recv_type = receiver_rbs_type_name(recv, opts[:core_rbs_provider], opts[:local_var_types],
696
+ opts[:param_types])
697
+ return nil unless recv_type
698
+
699
+ opts[:signature_provider].signature_for(container: recv_type, scope: :instance, name: meth)&.return_type
700
+ end
701
+
702
+ # Resolve return type from the current method's container via RBS.
703
+ #
704
+ # Handles implicit self calls (recv is nil) by looking up the method
705
+ # on the container class.
706
+ #
707
+ # @note module_function: defines #container_rbs_return_type (visibility: private)
708
+ # @param [Symbol] meth the method name being called
709
+ # @param [Object] opts additional keyword options (must include :container and :core_rbs_provider)
710
+ # @return [String, nil] resolved type or nil if unresolvable
711
+ def container_rbs_return_type(meth, **opts)
712
+ return unless opts[:container]
713
+
714
+ if opts[:core_rbs_provider]
715
+ rbs = resolve_rbs_return_type(opts[:container], meth, opts[:core_rbs_provider])
716
+ return rbs unless rbs == FALLBACK_TYPE
717
+ end
718
+
719
+ if opts[:signature_provider]
720
+ sig = opts[:signature_provider].signature_for(container: opts[:container], scope: :instance, name: meth)
721
+ return sig.return_type if sig
722
+ end
723
+
724
+ nil
725
+ end
726
+
727
+ # Map a receiver AST node to its RBS type name string.
728
+ #
729
+ # Supports local variables, method calls, literals, and instance/global/class variables.
730
+ #
731
+ # @note module_function: when included, also defines #receiver_rbs_type_name (instance visibility: private)
732
+ # @param [Parser::AST::Node, nil] recv the receiver node
733
+ # @param [Object, nil] core_rbs_provider core RBS provider
734
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable types
735
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
736
+ # @return [String, nil]
737
+ LITERAL_RBS_TYPES = {
738
+ int: 'Integer', str: 'String', sym: 'Symbol', true: 'Boolean',
739
+ false: 'Boolean', float: 'Float', array: 'Array', hash: 'Hash',
740
+ nil: 'NilClass'
741
+ }.freeze
742
+
743
+ # Map receiver AST node to RBS type name.
744
+ #
745
+ # @note module_function: defines #receiver_rbs_type_name (visibility: private)
746
+ # @param [Parser::AST::Node, nil] recv the receiver AST node
747
+ # @param [Object, nil] core_rbs_provider core RBS type provider
748
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable types
749
+ # @param [Hash<String, String>, nil] param_types parameter name-to-type map
750
+ # @return [String, nil]
751
+ def receiver_rbs_type_name(recv, core_rbs_provider, local_var_types, param_types)
752
+ return unless recv
753
+ return LITERAL_RBS_TYPES[recv.type] if LITERAL_RBS_TYPES.key?(recv.type)
754
+ return lookup_lvar_type(recv.children.first, local_var_types, param_types) if %i[lvar ivar gvar
755
+ cvar].include?(recv.type)
756
+ return unless recv.type == :send
757
+
758
+ run_last_expr_type(recv, fallback_type: FALLBACK_TYPE, nil_as_optional: false,
759
+ core_rbs_provider: core_rbs_provider,
760
+ param_types: param_types,
761
+ local_var_types: local_var_types)
762
+ end
763
+
764
+ # Infer return type from a compound-assignment-like `:send` by reading the
765
+ # first literal argument's type — only fires when `core_rbs_provider` is
766
+ # present and the argument's RBS return type can be resolved.
767
+ #
768
+ # Enables `@var += 123` -> `Integer` (via `Integer#+`) and similar patterns.
769
+ #
770
+ # @note module_function: defines #infer_from_compound_assign (visibility: private)
771
+ # @param [Parser::AST::Node] node the `:send` AST node
772
+ # @param [Object] opts additional keyword options forwarded to type inference
773
+ # @return [String, nil]
774
+ def infer_from_compound_assign(node, **opts)
775
+ return nil unless opts[:core_rbs_provider]
776
+
777
+ meth = node.children[1]
778
+ return nil unless %i[+ - * / % ** << | & ^].include?(meth)
779
+
780
+ first_arg = node.children[2]
781
+ return nil unless first_arg
782
+
783
+ arg_type = type_from_literal_safe(first_arg)
784
+ return nil unless arg_type
785
+
786
+ rbs = resolve_rbs_return_type(arg_type, meth, opts[:core_rbs_provider])
787
+ rbs unless rbs == FALLBACK_TYPE
788
+ end
789
+
790
+ # Safely get a type string from a literal node, returning nil if the node
791
+ # is not a literal or yields no type.
792
+ #
793
+ # @note module_function: defines #type_from_literal_safe (visibility: private)
794
+ # @param [Parser::AST::Node, nil] node literal AST node
795
+ # @return [String, nil]
796
+ def type_from_literal_safe(node)
797
+ return nil unless node
798
+
799
+ t = Literals.type_from_literal(node, fallback_type: FALLBACK_TYPE)
800
+ t unless t == FALLBACK_TYPE
328
801
  end
329
802
 
330
803
  # Resolve RBS return type for an `:lvar` receiver.
331
804
  #
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
805
+ # @note module_function: defines #resolve_lvar_rbs (visibility: private)
806
+ # @param [Parser::AST::Node?] recv the receiver node of the send
807
+ # @param [Symbol] meth the method name being called
808
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
809
+ # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
810
+ # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
339
811
  # @return [String, nil]
340
812
  def resolve_lvar_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
341
813
  lvar_name = recv&.children&.first
@@ -348,10 +820,10 @@ module Docscribe
348
820
 
349
821
  # Look up a local variable's inferred type from local or parameter type maps.
350
822
  #
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
823
+ # @note module_function: defines #lookup_lvar_type (visibility: private)
824
+ # @param [Object] lvar_name the local variable name
825
+ # @param [Hash<Object, Object>, nil] local_var_types inferred local variable type map
826
+ # @param [Hash<String, String>, nil] param_types parameter name to type map
355
827
  # @return [String, nil]
356
828
  def lookup_lvar_type(lvar_name, local_var_types, param_types)
357
829
  return local_var_types[lvar_name.to_s] if local_var_types&.key?(lvar_name.to_s)
@@ -362,13 +834,12 @@ module Docscribe
362
834
 
363
835
  # Resolve RBS return type for a chained `:send` receiver.
364
836
  #
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
837
+ # @note module_function: defines #resolve_chained_send_rbs (visibility: private)
838
+ # @param [Parser::AST::Node?] recv the receiver node of the send
839
+ # @param [Symbol] meth the method name being called
840
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
841
+ # @param [Hash<Object, Object>, nil] local_var_types pre-built local variable types map
842
+ # @param [Hash<String, String>, nil] param_types parameter name -> type map for lvar resolution
372
843
  # @return [String, nil]
373
844
  def resolve_chained_send_rbs(recv, meth, core_rbs_provider, local_var_types, param_types)
374
845
  inner_type = run_last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
@@ -390,14 +861,9 @@ module Docscribe
390
861
  # - literal-like expressions via {Literals.type_from_literal}
391
862
  # - method calls with RBS core type lookup
392
863
  #
393
- # @note module_function: when included, also defines #last_expr_type (instance visibility: private)
864
+ # @note module_function: defines #last_expr_type (visibility: private)
394
865
  # @param [Parser::AST::Node, nil] node expression node
395
- # @param [String] fallback_type type used when inference is uncertain
396
- # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
397
- # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
398
- # @param [Hash, nil] param_types parameter name -> type map for lvar resolution
399
- # @param [nil] local_var_types pre-built local variable types map
400
- # @param [Hash] opts additional keyword options forwarded to type inference
866
+ # @param [Object] opts additional keyword options forwarded to type inference
401
867
  # @return [String, nil]
402
868
  def last_expr_type(node, **opts)
403
869
  run_last_expr_type(node, **opts)
@@ -405,16 +871,17 @@ module Docscribe
405
871
 
406
872
  # Dispatch `last_expr_type` based on node type.
407
873
  #
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
874
+ # @note module_function: defines #run_last_expr_type (visibility: private)
875
+ # @param [Parser::AST::Node, nil] node the `:return` AST node
876
+ # @param [Object] opts options passed through as keyword args
411
877
  # @return [String, nil]
412
878
  def run_last_expr_type(node, **opts)
413
879
  return unless node
414
880
 
415
- handler = LAST_EXPR_TYPE_HANDLERS[node.type]
416
- if handler
417
- send(handler, node, **opts)
881
+ type = node.type == :defined? ? :defined : node.type
882
+ method_name = :"handle_#{type}_node"
883
+ if respond_to?(method_name, true)
884
+ send(method_name, node, **opts)
418
885
  else
419
886
  Literals.type_from_literal(node, fallback_type: opts[:fallback_type])
420
887
  end
@@ -422,9 +889,9 @@ module Docscribe
422
889
 
423
890
  # Extract the return type from an explicit `:return` node.
424
891
  #
425
- # @note module_function: when included, also defines #handle_return_node (instance visibility: private)
892
+ # @note module_function: defines #handle_return_node (visibility: private)
426
893
  # @param [Parser::AST::Node] node the `:return` AST node
427
- # @param [Hash] opts additional keyword options forwarded to type inference
894
+ # @param [Object] opts additional keyword options forwarded to type inference
428
895
  # @return [String, nil]
429
896
  def handle_return_node(node, **opts)
430
897
  Literals.type_from_literal(node.children.first, fallback_type: opts[:fallback_type])
@@ -432,10 +899,10 @@ module Docscribe
432
899
 
433
900
  # Resolve an RBS return type for a method call.
434
901
  #
435
- # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
902
+ # @note module_function: defines #resolve_rbs_return_type (visibility: private)
436
903
  # @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
904
+ # @param [String, Symbol] method_name method name
905
+ # @param [Object, nil] core_rbs_provider core RBS type lookup provider
439
906
  # @return [String] inferred return type
440
907
  def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
441
908
  return FALLBACK_TYPE unless core_rbs_provider
@@ -456,37 +923,34 @@ module Docscribe
456
923
  # - `nil` unions may become optional types if enabled
457
924
  # - otherwise falls back conservatively to `fallback_type`
458
925
  #
459
- # @note module_function: when included, also defines #unify_types (instance visibility: private)
460
- # @param [String, nil] a
461
- # @param [String, nil] b
462
- # @param [String] fallback_type
463
- # @param [Boolean] nil_as_optional
926
+ # @note module_function: defines #unify_types (visibility: private)
464
927
  # @param [String, nil] type_a first type to unify
465
928
  # @param [String, nil] type_b second type to unify
466
- # @return [String, nil]
929
+ # @param [String] fallback_type type used when neither is nil
930
+ # @param [Boolean] nil_as_optional whether to render nil unions as optional types
931
+ # @return [String]
467
932
  def unify_types(type_a, type_b, fallback_type:, nil_as_optional:)
468
933
  type_a ||= fallback_type
469
934
  type_b ||= fallback_type
470
935
  return type_a if type_a == type_b
471
936
 
472
- unify_nil_types(type_a, type_b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
937
+ unify_nil_types(type_a, type_b, nil_as_optional: nil_as_optional)
473
938
  end
474
939
 
475
940
  # Unify two types where one may be `nil`, producing optional or union type.
476
941
  #
477
- # @note module_function: when included, also defines #unify_nil_types (instance visibility: private)
942
+ # @note module_function: defines #unify_nil_types (visibility: private)
478
943
  # @param [String] type_a first type string
479
944
  # @param [String] type_b second type string
480
- # @param [String] fallback_type type used when neither is nil
481
945
  # @param [Boolean] nil_as_optional whether to render nil unions as optional types
482
946
  # @return [String]
483
- def unify_nil_types(type_a, type_b, fallback_type:, nil_as_optional:)
947
+ def unify_nil_types(type_a, type_b, nil_as_optional:)
484
948
  if type_a == 'nil' || type_b == 'nil'
485
949
  non_nil = (type_a == 'nil' ? type_b : type_a)
486
950
  return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
487
951
  end
488
952
 
489
- fallback_type
953
+ "#{type_a}, #{type_b}"
490
954
  end
491
955
  end
492
956
  end