From 13852ae37b0b4b1eb013a032d1764cde0a508a83 Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 16 Sep 2026 09:04:20 -0400 Subject: [PATCH 1/2] Collect block values in branches and returns A call with a block written at the call site contributed no type when it was the value of a `return` or of a conditional branch, so a method whose return value takes that shape got no inferred type at all. Two collectors disagreed about `:block`. from_value_position_statement, which handles a method body's tail expression, treats a block as a first class value via FUNCTION_VALUE. reduce_to_value_nodes, which handles if/unless branches, case branches and `return`, kept only the explicit returns found inside the block body and discarded the block itself. Push the block node there as well. The change is additive: a `return` inside a block returns from the enclosing method, so the block's own value and any inner explicit returns are both genuine possible values, and the existing explicit_return_values_from_compound_statement call stays alongside it. Two @sg-ignore markers the change obsoletes are removed, including the one recording that FlowSensitiveTyping#find_var had no inferrable return type - its body is an if/else whose branches are block calls, which is exactly this defect. `:numblock` is left alone. Both collectors already push it as a value through their fallthrough, yet it still infers as undefined, because NodeChainer recognises only `:block` and the type appears nowhere else in lib/. Supporting it is a separate change. --- .../parser/flow_sensitive_typing.rb | 2 -- .../parser/parser_gem/node_methods.rb | 5 ++- spec/parser/node_methods_spec.rb | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/parser/flow_sensitive_typing.rb b/lib/solargraph/parser/flow_sensitive_typing.rb index 1606a32e06..442d512ff7 100644 --- a/lib/solargraph/parser/flow_sensitive_typing.rb +++ b/lib/solargraph/parser/flow_sensitive_typing.rb @@ -294,8 +294,6 @@ def parse_isa isa_node # @param variable_name [String] # @param position [Position] # - # @sg-ignore Solargraph::Parser::FlowSensitiveTyping#find_var - # return type could not be inferred # @return [Solargraph::Pin::LocalVariable, Solargraph::Pin::InstanceVariable, nil] def find_var variable_name, position if variable_name.start_with?('@') diff --git a/lib/solargraph/parser/parser_gem/node_methods.rb b/lib/solargraph/parser/parser_gem/node_methods.rb index 59f2f255c6..d19013896d 100644 --- a/lib/solargraph/parser/parser_gem/node_methods.rb +++ b/lib/solargraph/parser/parser_gem/node_methods.rb @@ -615,11 +615,14 @@ def reduce_to_value_nodes nodes result.concat reduce_to_value_nodes(node.children) # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :block + # the block itself is a first class value that could be returned + result.push node + # a return inside the block returns from the enclosing method, + # so those values are possible here as well # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat explicit_return_values_from_compound_statement(node.children[2]) # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check elsif node.type == :resbody - # @sg-ignore flow sensitive typing needs to narrow down type with an if is_a? check result.concat reduce_to_value_nodes([node.children[2]]) else result.push node diff --git a/spec/parser/node_methods_spec.rb b/spec/parser/node_methods_spec.rb index 1ead0a6b61..adca272045 100644 --- a/spec/parser/node_methods_spec.rb +++ b/spec/parser/node_methods_spec.rb @@ -181,6 +181,37 @@ def parse source expect(rets.map(&:type)).to eq(%i[block lvar]) end + it 'collects a block value after an explicit return' do + node = parse(%( + return array.map { |item| item } + )) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[block]) + end + + it 'collects a block value in a conditional branch' do + node = parse(%( + if foo + array.map { |item| item } + else + 1 + end + )) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[block int]) + end + + it 'collects both a returned block value and returns inside it' do + node = parse(%( + return array.map { |item| + return item if foo + item + } + )) + rets = described_class.returns_from_method_body(node) + expect(rets.map(&:type)).to eq(%i[block lvar]) + end + it 'finds correct return node line in begin expressions' do node = parse(%( begin From 656f78dbdea57c5161cb69afc57ebed6f3e3cfff Mon Sep 17 00:00:00 2001 From: Vince Broz Date: Wed, 16 Sep 2026 11:11:57 -0400 Subject: [PATCH 2/2] Mark the masgn narrowing gap the fix surfaced Collecting block values from branches gives `pin` a real type in MasgnNode#process, where it was previously undefined and absorbed every call made on it. The typechecker now reports: Unresolved call to mass_assignment= `pins.find { |iv| ... && iv.is_a?(Pin::BaseVariable) }` types as Pin::Base, since the block's is_a? guard does not narrow what find returns, and mass_assignment is an attr_accessor on Pin::BaseVariable. At runtime the guard does guarantee it, so the code is correct and the gap is in the tool. Mark it with the reason string already used for this gap elsewhere rather than coining a new one. Remove the @todo above it asking for an alert when a non-existent method is called there. That alert is exactly what now fires, so the note is satisfied; leaving it would tell the next reader the check is still missing. Typecheck goes from 531 problems to 530 against the branch point. --- lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb b/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb index b5a7805d95..36daca6fc6 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb @@ -43,14 +43,13 @@ def process else pins.find { |iv| iv.location == location && iv.is_a?(Pin::BaseVariable) } end - # @todo in line below, nothing in typechecking alerts - # when a non-existant method is called on 'l' if pin.nil? Solargraph.logger.debug do "Could not find local for masgn= value in location #{location.inspect} in #{lhs_arr} - masgn = #{masgn}, lhs.type = #{lhs.type}" end next end + # @sg-ignore flow sensitive typing needs to infer Enumerable#find's block return type from an is_a? check pin.mass_assignment = [mass_rhs, i] end end