Module: Docscribe::Infer::Returns
- Defined in:
- lib/docscribe/infer/returns.rb
Overview
Return type inference and rescue-conditional return extraction.
Class Method Summary collapse
-
.build_local_variable_types(node) ⇒ String
Resolve a return type from core RBS for a method call.
-
.infer_return_type(method_source) ⇒ String
Infer a return type from a full method definition source string.
-
.infer_return_type_from_node(node) ⇒ String
Infer a method’s normal return type from an already parsed def/defs node.
-
.last_expr_type(node, fallback_type:, nil_as_optional:, core_rbs_provider: nil, param_types: nil, local_var_types: nil) ⇒ String?
Infer the type of the last expression in a node.
-
.resolve_rbs_return_type(container_type, method_name, core_rbs_provider) ⇒ Object
Method documentation.
-
.returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, param_types: nil) ⇒ Hash
Return a structured return-type spec for a method node.
-
.unify_types(a, b, fallback_type:, nil_as_optional:) ⇒ String?
Unify two inferred types into a single type string.
Class Method Details
.build_local_variable_types(node) ⇒ String
module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
Resolve a return type from core RBS for a method call.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/docscribe/infer/returns.rb', line 116 def build_local_variable_types(node) types = {} ASTWalk.walk(node) do |n| case n.type when :lvasgn, :gvasgn, :ivasgn name = n.children[0].to_s value = n.children[1] if value inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE) types[name] = inferred if inferred && inferred != FALLBACK_TYPE end when :casgn name = n.children[0].to_s value = n.children[2] if value inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE) types[name] = inferred if inferred && inferred != FALLBACK_TYPE end end end types.empty? ? nil : types end |
.infer_return_type(method_source) ⇒ String
module_function: when included, also defines #infer_return_type (instance visibility: private)
Infer a return type from a full method definition source string.
The source must parse to a ‘:def` or `:defs` node. If parsing fails or inference is uncertain, the fallback type is returned.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/docscribe/infer/returns.rb', line 18 def infer_return_type(method_source) return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty? buffer = Parser::Source::Buffer.new('(method)') buffer.source = method_source root = Docscribe::Parsing.parse_buffer(buffer) return FALLBACK_TYPE unless root && %i[def defs].include?(root.type) body = root.children.last local_var_types = build_local_variable_types(body) last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true, local_var_types: local_var_types) || FALLBACK_TYPE rescue Parser::SyntaxError FALLBACK_TYPE end |
.infer_return_type_from_node(node) ⇒ String
module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
Infer a method’s normal return type from an already parsed def/defs node.
39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/docscribe/infer/returns.rb', line 39 def infer_return_type_from_node(node) body = case node.type when :def then node.children[2] when :defs then node.children[3] end return FALLBACK_TYPE unless body local_var_types = build_local_variable_types(body) last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true, local_var_types: local_var_types) || FALLBACK_TYPE end |
.last_expr_type(node, fallback_type:, nil_as_optional:, core_rbs_provider: nil, param_types: nil, local_var_types: nil) ⇒ String?
module_function: when included, also defines #last_expr_type (instance visibility: private)
Infer the type of the last expression in a node.
Supports:
-
‘begin` groups
-
‘if` branches
-
‘case` expressions
-
explicit ‘return`
-
literal-like expressions via Literals.type_from_literal
-
method calls with RBS core type lookup
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/docscribe/infer/returns.rb', line 157 def last_expr_type(node, fallback_type:, nil_as_optional:, core_rbs_provider: nil, param_types: nil, local_var_types: nil) return nil unless node case node.type when :begin last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) when :if t = last_expr_type(node.children[1], fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) e = last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) unify_types(t, e, fallback_type: fallback_type, nil_as_optional: nil_as_optional) when :case branches = node.children[1..].compact.flat_map do |child| if child.type == :when last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) else last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) end end.compact if branches.empty? fallback_type else branches.reduce do |a, b| unify_types(a, b, fallback_type: fallback_type, nil_as_optional: nil_as_optional) end end when :return Literals.type_from_literal(node.children.first, fallback_type: fallback_type) when :block send_node = node.children[0] if send_node&.type == :send recv = send_node.children[0] meth = send_node.children[1] if core_rbs_provider && recv&.type == :lvar lvar_name = recv.children.first recv_type = nil recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name if recv_type rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider) return rbs_type unless rbs_type == FALLBACK_TYPE end elsif core_rbs_provider && recv&.type == :send inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) if inner_type rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider) return rbs_type unless rbs_type == FALLBACK_TYPE end end end last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) when :send recv = node.children[0] meth = node.children[1] # Try to resolve return type from RBS core for method calls if core_rbs_provider && recv&.type == :send # Chained call: arg.to_i.positive? inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) if inner_type rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider) return rbs_type unless rbs_type == FALLBACK_TYPE end elsif core_rbs_provider && recv&.type == :lvar # Direct call on local variable: p1.positive? or admins.any? lvar_name = recv.children.first recv_type = nil recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name if recv_type rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider) return rbs_type unless rbs_type == FALLBACK_TYPE end end Literals.type_from_literal(node, fallback_type: fallback_type) else Literals.type_from_literal(node, fallback_type: fallback_type) end end |
.resolve_rbs_return_type(container_type, method_name, core_rbs_provider) ⇒ Object
module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
Method documentation.
271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/docscribe/infer/returns.rb', line 271 def resolve_rbs_return_type(container_type, method_name, core_rbs_provider) return FALLBACK_TYPE unless core_rbs_provider sig = core_rbs_provider.signature_for( container: container_type, scope: :instance, name: method_name ) sig&.return_type || FALLBACK_TYPE end |
.returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, param_types: nil) ⇒ Hash
module_function: when included, also defines #returns_spec_from_node (instance visibility: private)
Return a structured return-type spec for a method node.
The result includes:
-
‘:normal` => normal/happy-path return type
-
‘:rescues` => array of `[exception_names, return_type]` pairs for rescue branches
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/docscribe/infer/returns.rb', line 66 def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil, param_types: nil) body = case node.type when :def then node.children[2] when :defs then node.children[3] end spec = { normal: FALLBACK_TYPE, rescues: [] } return spec unless body local_var_types = build_local_variable_types(body) if body.type == :rescue main_body = body.children[0] rescue_local_var_types = build_local_variable_types(body) all_local_var_types = rescue_local_var_types || local_var_types spec[:normal] = last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: all_local_var_types) || FALLBACK_TYPE body.children.each do |ch| next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody exc_list, _asgn, rescue_body = *ch exc_names = Raises.exception_names_from_rescue_list(exc_list) rtype = last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: all_local_var_types) || fallback_type spec[:rescues] << [exc_names, rtype] end else spec[:normal] = last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional, core_rbs_provider: core_rbs_provider, param_types: param_types, local_var_types: local_var_types) || FALLBACK_TYPE end spec end |
.unify_types(a, b, fallback_type:, nil_as_optional:) ⇒ String?
module_function: when included, also defines #unify_types (instance visibility: private)
Unify two inferred types into a single type string.
Rules:
-
identical types remain unchanged
-
‘nil` unions may become optional types if enabled
-
otherwise falls back conservatively to ‘fallback_type`
296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'lib/docscribe/infer/returns.rb', line 296 def unify_types(a, b, fallback_type:, nil_as_optional:) a ||= fallback_type b ||= fallback_type return a if a == b if a == 'nil' || b == 'nil' non_nil = (a == 'nil' ? b : a) return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil" end fallback_type end |