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