Skip to content

Commit a8c4b23

Browse files
authored
Infer block-pass symbols (#793)
* BlockSymbol link * First iteration of block-pass symbol resolution * Chain::Call#block * Superfluous block tracking * Refactor NodeChainer * Fix chain block check * Sync with #780 * BlockSymbol link * First iteration of block-pass symbol resolution * Change spec to test Call#block * Resolve generics from BlockSymbol
1 parent 3bf58b7 commit a8c4b23

File tree

11 files changed

+142
-72
lines changed

11 files changed

+142
-72
lines changed

lib/solargraph/parser/parser_gem/node_chainer.rb

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ class NodeChainer
1111

1212
# @param node [Parser::AST::Node]
1313
# @param filename [String, nil]
14-
# @param in_block [Boolean]
15-
def initialize node, filename = nil, in_block = false
14+
# @param parent [Parser::AST::Node, nil]
15+
def initialize node, filename = nil, parent = nil
1616
@node = node
1717
@filename = filename
18-
@in_block = in_block ? 1 : 0
18+
@parent = parent
1919
end
2020

2121
# @return [Source::Chain]
@@ -27,10 +27,10 @@ def chain
2727
class << self
2828
# @param node [Parser::AST::Node]
2929
# @param filename [String, nil]
30-
# @param in_block [Boolean]
30+
# @param parent [Parser::AST::Node, nil]
3131
# @return [Source::Chain]
32-
def chain node, filename = nil, in_block = false
33-
NodeChainer.new(node, filename, in_block).chain
32+
def chain node, filename = nil, parent = nil
33+
NodeChainer.new(node, filename, parent).chain
3434
end
3535

3636
# @param code [String]
@@ -52,53 +52,39 @@ def generate_links n
5252
return generate_links(n.children[0]) if n.type == :splat
5353
result = []
5454
if n.type == :block
55-
@in_block += 1
56-
result.concat generate_links(n.children[0])
57-
@in_block -= 1
55+
result.concat NodeChainer.chain(n.children[0], @filename, n).links
5856
elsif n.type == :send
5957
if n.children[0].is_a?(::Parser::AST::Node)
6058
result.concat generate_links(n.children[0])
61-
args = []
62-
n.children[2..-1].each do |c|
63-
args.push NodeChainer.chain(c)
64-
end
65-
result.push Chain::Call.new(n.children[1].to_s, args, @in_block > 0 || block_passed?(n))
59+
result.push Chain::Call.new(n.children[1].to_s, node_args(n), passed_block(n))
6660
elsif n.children[0].nil?
6761
args = []
6862
n.children[2..-1].each do |c|
69-
args.push NodeChainer.chain(c)
63+
args.push NodeChainer.chain(c, @filename, n)
7064
end
71-
result.push Chain::Call.new(n.children[1].to_s, args, @in_block > 0 || block_passed?(n))
65+
result.push Chain::Call.new(n.children[1].to_s, node_args(n), passed_block(n))
7266
else
7367
raise "No idea what to do with #{n}"
7468
end
7569
elsif n.type == :csend
7670
if n.children[0].is_a?(::Parser::AST::Node)
7771
result.concat generate_links(n.children[0])
78-
args = []
79-
n.children[2..-1].each do |c|
80-
args.push NodeChainer.chain(c)
81-
end
82-
result.push Chain::QCall.new(n.children[1].to_s, args, @in_block > 0 || block_passed?(n))
72+
result.push Chain::QCall.new(n.children[1].to_s, node_args(n))
8373
elsif n.children[0].nil?
84-
args = []
85-
n.children[2..-1].each do |c|
86-
args.push NodeChainer.chain(c)
87-
end
88-
result.push Chain::QCall.new(n.children[1].to_s, args, @in_block > 0 || block_passed?(n))
74+
result.push Chain::QCall.new(n.children[1].to_s, node_args(n))
8975
else
9076
raise "No idea what to do with #{n}"
9177
end
9278
elsif n.type == :self
9379
result.push Chain::Head.new('self')
9480
elsif n.type == :zsuper
95-
result.push Chain::ZSuper.new('super', @in_block > 0 || block_passed?(n))
81+
result.push Chain::ZSuper.new('super')
9682
elsif n.type == :super
97-
args = n.children.map { |c| NodeChainer.chain(c) }
98-
result.push Chain::Call.new('super', args, @in_block > 0 || block_passed?(n))
83+
args = n.children.map { |c| NodeChainer.chain(c, @filename, n) }
84+
result.push Chain::Call.new('super', args)
9985
elsif n.type == :yield
100-
args = n.children.map { |c| NodeChainer.chain(c) }
101-
result.push Chain::Call.new('yield', args, @in_block > 0 || block_passed?(n))
86+
args = n.children.map { |c| NodeChainer.chain(c, @filename, n) }
87+
result.push Chain::Call.new('yield', args)
10288
elsif n.type == :const
10389
const = unpack_name(n)
10490
result.push Chain::Constant.new(const)
@@ -118,7 +104,7 @@ def generate_links n
118104
elsif n.type == :and
119105
result.concat generate_links(n.children.last)
120106
elsif n.type == :or
121-
result.push Chain::Or.new([NodeChainer.chain(n.children[0], @filename), NodeChainer.chain(n.children[1], @filename)])
107+
result.push Chain::Or.new([NodeChainer.chain(n.children[0], @filename, n), NodeChainer.chain(n.children[1], @filename, n)])
122108
elsif [:begin, :kwbegin].include?(n.type)
123109
result.concat generate_links(n.children.last)
124110
elsif n.type == :block_pass
@@ -128,7 +114,11 @@ def generate_links n
128114
# added in Ruby 3.1 - https://bugs.ruby-lang.org/issues/11256
129115
result.push Chain::BlockVariable.new(nil)
130116
else
131-
result.push Chain::BlockVariable.new("&#{block_variable_name_node.children[0].to_s}")
117+
if block_variable_name_node.type == :sym
118+
result.push Chain::BlockSymbol.new("#{block_variable_name_node.children[0].to_s}")
119+
else
120+
result.push Chain::BlockVariable.new("&#{block_variable_name_node.children[0].to_s}")
121+
end
132122
end
133123
elsif n.type == :hash
134124
result.push Chain::Hash.new('::Hash', hash_is_splatted?(n))
@@ -150,9 +140,16 @@ def hash_is_splatted? node
150140
true
151141
end
152142

153-
# @param node [Parser::AST::Node]
154-
def block_passed? node
155-
node.children.last.is_a?(::Parser::AST::Node) && node.children.last.type == :block_pass
143+
def passed_block node
144+
return unless node == @node && @parent&.type == :block
145+
146+
NodeChainer.chain(@parent.children[2], @filename)
147+
end
148+
149+
def node_args node
150+
node.children[2..-1].map do |child|
151+
NodeChainer.chain(child, @filename, node)
152+
end
156153
end
157154
end
158155
end

lib/solargraph/pin/base_variable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def probe api_map
5050
clip = api_map.clip_at(location.filename, pos)
5151
# Use the return node for inference. The clip might infer from the
5252
# first node in a method call instead of the entire call.
53-
chain = Parser.chain(node, nil, clip.in_block?)
53+
chain = Parser.chain(node, nil, nil)
5454
result = chain.infer(api_map, closure, clip.locals)
5555
types.push result unless result.undefined?
5656
end

lib/solargraph/pin/method.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def param_type_from_name(tag, name)
346346

347347
# @return [ComplexType]
348348
def generate_complex_type
349-
tags = docstring.tags(:return).map(&:types).flatten.reject(&:nil?)
349+
tags = docstring.tags(:return).map(&:types).flatten.compact
350350
return ComplexType::UNDEFINED if tags.empty?
351351
ComplexType.try_parse *tags
352352
end

lib/solargraph/source.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -435,23 +435,16 @@ def string_nodes_in n
435435
def inner_tree_at node, position, stack
436436
return if node.nil?
437437
here = Range.from_node(node)
438-
if here.contain?(position) || colonized(here, position, node)
438+
if here.contain?(position)
439439
stack.unshift node
440440
node.children.each do |c|
441441
next unless Parser.is_ast_node?(c)
442-
next if !Parser.rubyvm? && c.loc.expression.nil?
442+
next if c.loc.expression.nil?
443443
inner_tree_at(c, position, stack)
444444
end
445445
end
446446
end
447447

448-
def colonized range, position, node
449-
node.type == :COLON2 &&
450-
range.ending.line == position.line &&
451-
range.ending.character == position.character - 2 &&
452-
code[Position.to_offset(code, Position.new(position.line, position.character - 2)), 2] == '::'
453-
end
454-
455448
protected
456449

457450
# @return [String]

lib/solargraph/source/chain.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Chain
2121
autoload :Head, 'solargraph/source/chain/head'
2222
autoload :Or, 'solargraph/source/chain/or'
2323
autoload :BlockVariable, 'solargraph/source/chain/block_variable'
24+
autoload :BlockSymbol, 'solargraph/source/chain/block_symbol'
2425
autoload :ZSuper, 'solargraph/source/chain/z_super'
2526
autoload :Hash, 'solargraph/source/chain/hash'
2627
autoload :Array, 'solargraph/source/chain/array'
@@ -168,13 +169,15 @@ def infer_first_defined pins, context, api_map, locals
168169
@@inference_depth -= 1
169170
end
170171
return ComplexType::UNDEFINED if possibles.empty?
172+
171173
type = if possibles.length > 1
172174
sorted = possibles.map { |t| t.rooted? ? "::#{t}" : t.to_s }.sort { |a, _| a == 'nil' ? 1 : 0 }
173175
ComplexType.parse(*sorted)
174176
else
175177
ComplexType.parse(possibles.map(&:to_s).join(', '))
176178
end
177179
return type if context.nil? || context.return_type.undefined?
180+
178181
type.self_to(context.return_type.tag)
179182
end
180183

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module Solargraph
4+
class Source
5+
class Chain
6+
class BlockSymbol < Link
7+
def resolve api_map, name_pin, locals
8+
[Pin::ProxyType.anonymous(ComplexType.try_parse('Proc'))]
9+
end
10+
end
11+
end
12+
end
13+
end

lib/solargraph/source/chain/call.rb

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@ class Call < Link
1010
# @return [::Array<Chain>]
1111
attr_reader :arguments
1212

13+
# @return [Chain, nil]
14+
attr_reader :block
15+
1316
# @param word [String]
1417
# @param arguments [::Array<Chain>]
15-
# @param with_block [Boolean] True if the chain is inside a block
16-
# @param head [Boolean] True if the call is the start of its chain
17-
def initialize word, arguments = [], with_block = false
18+
# @param block [Chain, nil]
19+
def initialize word, arguments = [], block = nil
1820
@word = word
1921
@arguments = arguments
20-
@with_block = with_block
22+
@block = block
23+
fix_block_pass
2124
end
2225

2326
def with_block?
24-
@with_block
27+
!!@block
2528
end
2629

2730
# @param api_map [ApiMap]
@@ -74,16 +77,7 @@ def inferred_pins pins, api_map, context, locals
7477
param = ol.parameters[idx]
7578
atype = nil
7679
if param.nil?
77-
last_arg = idx == arguments.length - 1
78-
match = if ol.parameters.any?(&:restarg?)
79-
true
80-
elsif last_arg && ol.block?
81-
# block argument that isn't declared as an arg as well - let's add that here
82-
atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(context), locals)
83-
atypes[idx].namespace == 'Proc'
84-
else
85-
false
86-
end
80+
match = ol.parameters.any?(&:restarg?)
8781
break
8882
end
8983
atype = atypes[idx] ||= arg.infer(api_map, Pin::ProxyType.anonymous(context), locals)
@@ -95,10 +89,16 @@ def inferred_pins pins, api_map, context, locals
9589
end
9690
end
9791
if match
98-
new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes)
99-
new_return_type = new_signature_pin.return_type
100-
type = with_params(new_return_type.self_to(context.to_s), context).qualify(api_map, context.namespace) if new_return_type.defined?
101-
type ||= ComplexType::UNDEFINED
92+
# @todo Functional but dodgy generic resolution from block inference
93+
if block && ol.block && ol.block.return_type.name == 'generic' && ol.return_type.to_s.include?(ol.block.return_type.to_s)
94+
blocktype = block_call_type(api_map, context)
95+
type = ComplexType.parse(ol.return_type.to_s.gsub("generic<#{ol.generics.first}>", blocktype.to_s))
96+
else
97+
new_signature_pin = ol.resolve_generics_from_context_until_complete(ol.generics, atypes)
98+
new_return_type = new_signature_pin.return_type
99+
type = with_params(new_return_type.self_to(context.to_s), context).qualify(api_map, context.namespace) if new_return_type.defined?
100+
type ||= ComplexType::UNDEFINED
101+
end
102102
end
103103
break if type.defined?
104104
end
@@ -234,6 +234,21 @@ def with_params type, context
234234
return type unless type.to_s.include?('$')
235235
ComplexType.try_parse(type.to_s.gsub('$', context.value_types.map(&:tag).join(', ')).gsub('<>', ''))
236236
end
237+
238+
def fix_block_pass
239+
argument = @arguments.last&.links&.first
240+
@block = @arguments.pop if argument.is_a?(BlockSymbol) || argument.is_a?(BlockVariable)
241+
end
242+
243+
def block_call_type(api_map, context)
244+
return nil unless with_block?
245+
246+
# @todo Handle BlockVariable and literal blocks
247+
if block.links.first.is_a?(BlockSymbol)
248+
callee = api_map.get_path_pins("#{context.subtypes.first}##{block.links.first.word}").first
249+
callee&.return_type || ComplexType::UNDEFINED
250+
end
251+
end
237252
end
238253
end
239254
end

lib/solargraph/source/source_chainer.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ class SourceChainer
1414

1515
class << self
1616
# @param source [Source]
17-
# @param position [Position]
17+
# @param position [Position, Array(Integer, Integer)]
1818
# @return [Source::Chain]
1919
def chain source, position
20-
new(source, position).chain
20+
new(source, Solargraph::Position.normalize(position)).chain
2121
end
2222
end
2323

@@ -41,8 +41,6 @@ def chain
4141
parent = nil
4242
if !source.repaired? && source.parsed? && source.synchronized?
4343
tree = source.tree_at(position.line, position.column)
44-
# node, parent = source.tree_at(position.line, position.column)[0..2]
45-
tree.shift while tree.length > 1 && tree.first.type == :SCOPE
4644
node, parent = tree[0..2]
4745
elsif source.parsed? && source.repaired? && end_of_phrase == '.'
4846
node, parent = source.tree_at(fixed_position.line, fixed_position.column)[0..2]
@@ -60,7 +58,7 @@ def chain
6058
end
6159
return Chain.new([Chain::UNDEFINED_CALL]) if node.nil? || (node.type == :sym && !phrase.start_with?(':'))
6260
# chain = NodeChainer.chain(node, source.filename, parent && parent.type == :block)
63-
chain = Parser.chain(node, source.filename, parent && [:ITER, :block].include?(parent.type))
61+
chain = Parser.chain(node, source.filename, parent)
6462
if source.repaired? || !source.parsed? || !source.synchronized?
6563
if end_of_phrase.strip == '.'
6664
chain.links.push Chain::UNDEFINED_CALL

spec/parser/node_chainer_spec.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,26 @@ class Foo
109109
foo(bar, &baz)
110110
))
111111
chain = Solargraph::Parser.chain(source.node)
112-
expect(chain.links.first.arguments.length).to eq(2)
112+
expect(chain.links.first.arguments.length).to eq(1)
113+
expect(chain.links.first).to be_with_block
114+
end
115+
116+
it 'tracks block-pass symbols' do
117+
source = Solargraph::Source.load_string(%(
118+
foo(&:bar)
119+
))
120+
chain = Solargraph::Parser.chain(source.node)
121+
expect(chain.links.first.block).to be_a(Solargraph::Source::Chain)
122+
expect(chain.links.first.block.links.first).to be_a(Solargraph::Source::Chain::BlockSymbol)
123+
end
124+
125+
it 'tracks block-pass symbols' do
126+
source = Solargraph::Source.load_string(%(
127+
foo(&:bar)
128+
))
129+
chain = Solargraph::Parser.chain(source.node)
130+
arg = chain.links.first.block.links.first
131+
expect(arg).to be_a(Solargraph::Source::Chain::BlockSymbol)
113132
end
114133

115134
# feature added in Ruby 3.1

spec/source/source_chainer_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,14 @@ def strings; end
326326
expect(chain.node.type).to be(:send)
327327
expect(chain.node.children[1]).to be(:s)
328328
end
329+
330+
it 'adds blocks to calls' do
331+
source = Solargraph::Source.load_string(%(
332+
x.y do
333+
z
334+
end
335+
))
336+
chain = Solargraph::Source::SourceChainer.chain(source, [1, 9])
337+
expect(chain.links.last.block).to be
338+
end
329339
end

0 commit comments

Comments
 (0)