Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
strategy:
fail-fast: false
matrix:
prism: ['0.18.0']
prism: ['latest', '0.19.0']
env:
GEMFILE_PRISM_VERSION: ${{matrix.prism}}
steps:
Expand Down
26 changes: 9 additions & 17 deletions lib/repl_type_completor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,15 @@ def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
calculate_type_scope = ->(node) { TypeAnalyzer.calculate_target_type_scope binding, [*parents, target_node], node }

case target_node
when Prism::StringNode, Prism::InterpolatedStringNode
when Prism::StringNode
return unless target_node.closing&.empty?

call_node, args_node = parents.last(2)
return unless call_node.is_a?(Prism::CallNode) && call_node.receiver.nil?
return unless args_node.is_a?(Prism::ArgumentsNode) && args_node.arguments.size == 1

content = code.byteslice(target_node.opening_loc.end_offset..)
case call_node.name
when :require
[:require, content]
when :require_relative
[:require_relative, content]
if call_node.name == :require || call_node.name == :require_relative
[call_node.name, target_node.content]
end
when Prism::SymbolNode
return unless !target_node.closing || target_node.empty?
Expand All @@ -90,8 +88,8 @@ def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
else
[:symbol, name] unless name.empty?
end
when Prism::CallNode
return if target_node.opening
when Prism::CallNode, Prism::CallTargetNode
return if target_node.is_a?(Prism::CallNode) && target_node.opening

name = target_node.message.to_s
return [:lvar_or_method, name, calculate_scope.call] if target_node.receiver.nil?
Expand Down Expand Up @@ -127,14 +125,8 @@ def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
end

def find_target(node, position)
case node
when Prism::StringNode
# Unclosed quoted string has empty content and empty closing
return [node] if node.opening && node.closing&.empty?
when Prism::InterpolatedStringNode
# Unclosed double quoted string is InterpolatedStringNode with empty parts
return [node] if node.parts.empty? && node.opening && node.closing&.empty?
end
# Skip because NumberedParametersNode#location gives location of whole block
return if node.is_a? Prism::NumberedParametersNode

node.compact_child_nodes.each do |n|
match = find_target(n, position)
Expand Down
55 changes: 26 additions & 29 deletions lib/repl_type_completor/type_analyzer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ def evaluate_reference_read(node, scope)


def evaluate_call_node(node, scope)
is_field_assign = node.name.match?(/[^<>=!\]]=\z/) || (node.name == :[]= && !node.call_operator)
receiver_type = node.receiver ? evaluate(node.receiver, scope) : scope.self_type
evaluate_method = lambda do |scope|
args_types, kwargs_types, block_sym_node, has_block = evaluate_call_node_arguments node, scope
Expand All @@ -254,16 +253,15 @@ def evaluate_call_node(node, scope)
elsif node.block.is_a? Prism::BlockNode
call_block_proc = ->(block_args, block_self_type) do
scope.conditional do |s|
numbered_parameters = node.block.locals.grep(/\A_[1-9]/).map(&:to_s)
params_table = node.block.locals.to_h { [_1.to_s, Types::NIL] }
table = { **params_table, Scope::BREAK_RESULT => nil, Scope::NEXT_RESULT => nil }
block_scope = Scope.new s, table, self_type: block_self_type, trace_ivar: !block_self_type
# TODO kwargs
if node.block.parameters&.parameters
# node.block.parameters is Prism::BlockParametersNode
case node.block.parameters
when Prism::NumberedParametersNode
assign_numbered_parameters node.block.parameters.maximum, block_scope, block_args, {}
when Prism::BlockParametersNode
assign_parameters node.block.parameters.parameters, block_scope, block_args, {}
elsif !numbered_parameters.empty?
assign_numbered_parameters numbered_parameters, block_scope, block_args, {}
end
result = node.block.body ? evaluate(node.block.body, block_scope) : Types::NIL
block_scope.merge_jumps
Expand All @@ -281,7 +279,7 @@ def evaluate_call_node(node, scope)
call_block_proc = ->(_block_args, _self_type) { Types::OBJECT }
end
result = method_call receiver_type, node.name, args_types, kwargs_types, call_block_proc, scope
if is_field_assign
if node.attribute_write?
args_types.last || Types::NIL
else
result
Expand Down Expand Up @@ -583,8 +581,8 @@ def evaluate_rescue_node(node, scope)
case node.reference
when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode, Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode, Prism::ConstantTargetNode
s[node.reference.name.to_s] = error_type
when Prism::CallNode
evaluate node.reference, s
when Prism::CallTargetNode, Prism::IndexTargetNode
evaluate_multi_write_receiver node.reference, s, nil
end
end
node.statements ? evaluate(node.statements, s) : Types::NIL
Expand Down Expand Up @@ -904,7 +902,6 @@ def assign_parameters(node, scope, args, kwargs)
args = sized_splat(args.first, :to_ary, size) if size >= 2 && args.size == 1
reqs = args.shift node.requireds.size
if node.rest
# node.rest is Prism::RestParameterNode
posts = []
opts = args.shift node.optionals.size
rest = args
Expand All @@ -925,8 +922,8 @@ def assign_parameters(node, scope, args, kwargs)
node.posts.zip posts do |n, v|
assign_required_parameter n, v, scope
end
if node.rest&.name
# node.rest is Prism::RestParameterNode
# Prism::ImplicitRestNode (tap{|a,|}) does not have a name
if node.rest.is_a?(Prism::RestParameterNode) && node.rest.name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a if node.rest condition a few lines above, which seems to already expect it to be Prism::RestParameterNode? Perhaps we can merge that and this together?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the confusing comment in the if node.rest condition and add a comment about Prism::ImplicitRestNode that does not have name.

The if node.rest above part is separating given args to reqs, posts, rest
method do |a,b=1,*c| and given args [Integer, String, Symbol] to reqs = [Integer], opts = [String], posts = [], rest = [Symbol].

This part is assigning each of them.

node.requireds.zip{assign}
node.optionals.zip{assign}
node.posts.zip{assign}
if node.rest; assign; end

I want to separate them, not to merge it together.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the clear explanation 👍

scope[node.rest.name.to_s] = Types.array_of(*rest)
end
node.keywords.each do |n|
Expand All @@ -946,16 +943,13 @@ def assign_parameters(node, scope, args, kwargs)
end
end

def assign_numbered_parameters(numbered_parameters, scope, args, _kwargs)
return if numbered_parameters.empty?
max_num = numbered_parameters.map { _1[1].to_i }.max
if max_num == 1
def assign_numbered_parameters(maximum, scope, args, _kwargs)
if maximum == 1
scope['_1'] = args.first || Types::NIL
else
args = sized_splat(args.first, :to_ary, max_num) if args.size == 1
numbered_parameters.each do |name|
index = name[1].to_i - 1
scope[name] = args[index] || Types::NIL
args = sized_splat(args.first, :to_ary, maximum) if args.size == 1
maximum.times do |index|
scope["_#{index + 1}"] = args[index] || Types::NIL
end
end
end
Expand Down Expand Up @@ -1038,16 +1032,15 @@ def evaluate_write(node, value, scope, evaluated_receivers)
case node
when Prism::MultiTargetNode
evaluate_multi_write node, value, scope, evaluated_receivers
when Prism::CallNode
evaluated_receivers&.[](node.receiver) || evaluate(node.receiver, scope) if node.receiver
when Prism::CallTargetNode, Prism::IndexTargetNode
evaluated_receivers&.[](node.receiver) || evaluate_multi_write_receiver(node, scope, nil)
when Prism::SplatNode
evaluate_write node.expression, Types.array_of(value), scope, evaluated_receivers if node.expression
when Prism::LocalVariableTargetNode, Prism::GlobalVariableTargetNode, Prism::InstanceVariableTargetNode, Prism::ClassVariableTargetNode, Prism::ConstantTargetNode
scope[node.name.to_s] = value
when Prism::ConstantPathTargetNode
receiver = evaluated_receivers&.[](node.parent) || evaluate(node.parent, scope) if node.parent
const_path_write receiver, node.child.name.to_s, value, scope
value
end
end

Expand All @@ -1070,20 +1063,24 @@ def evaluate_multi_write_receiver(node, scope, evaluated_receivers)
when Prism::MultiWriteNode, Prism::MultiTargetNode
targets = [*node.lefts, *node.rest, *node.rights]
targets.each { evaluate_multi_write_receiver _1, scope, evaluated_receivers }
when Prism::CallNode
if node.receiver
receiver = evaluate(node.receiver, scope)
evaluated_receivers[node.receiver] = receiver if evaluated_receivers
end
when Prism::CallTargetNode, Prism::CallNode
receiver = evaluate(node.receiver, scope)
evaluated_receivers[node.receiver] = receiver if evaluated_receivers
receiver
when Prism::IndexTargetNode
receiver = evaluate(node.receiver, scope)
evaluated_receivers[node.receiver] = receiver if evaluated_receivers
if node.arguments
node.arguments.arguments&.each do |arg|
if arg.is_a? Prism::SplatNode
evaluate arg.expression, scope
evaluate arg.expression, scope if arg.expression
else
evaluate arg, scope
end
end
end
evaluate node.block.expression, scope if node.block&.expression
receiver
when Prism::SplatNode
evaluate_multi_write_receiver node.expression, scope, evaluated_receivers if node.expression
end
Expand Down
2 changes: 1 addition & 1 deletion repl_type_completor.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "prism", ">= 0.18.0", "< 0.19.0"
spec.add_dependency "prism", ">= 0.19.0", "< 0.20.0"
spec.add_dependency "rbs", ">= 2.7.0", "< 4.0.0"
end
8 changes: 5 additions & 3 deletions test/repl_type_completor/test_repl_type_completor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ def assert_doc_namespace(code, namespace, binding: empty_binding)
def test_require
assert_completion("require '", include: 'set')
assert_completion("require 's", include: 'et')
assert_completion('require "', include: 'set')
assert_completion('require "s', include: 'et')
assert_completion("require_relative 'test_", filename: __FILE__, include: 'repl_type_completor')
assert_completion("require_relative '../repl_", filename: __FILE__, include: 'type_completor/test_repl_type_completor')
Dir.chdir File.join(__dir__, '..') do
assert_completion("require_relative 'repl_", filename: nil, include: 'type_completor/test_repl_type_completor')
assert_completion("require_relative 'repl_", filename: '(irb)', include: 'type_completor/test_repl_type_completor')
end

# Incomplete double quote string is InterpolatedStringNode
assert_completion('require "', include: 'set')
assert_completion('require "s', include: 'et')
# Should not complete terminated string
assert_nil ReplTypeCompletor.analyze('require "s"', binding: empty_binding)
assert_nil ReplTypeCompletor.analyze('require ?s', binding: empty_binding)
end

def test_method_block_sym
Expand Down
9 changes: 9 additions & 0 deletions test/repl_type_completor/test_type_analyze.rb
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,15 @@ def test_massign
assert_call('a, ((b=1).c, d) = 1; b.', include: Integer)
assert_call('a, b[c=1] = 1; c.', include: Integer)
assert_call('a, b[*(c=1)] = 1; c.', include: Integer)
assert_call('a, b[**(c=1)] = 1; c.', include: Integer)
assert_call('a, b[&(c=1)] = 1; c.', include: Integer)
assert_call('a, b[] = 1; a.', include: Integer)
assert_call('def f(*); a, b[*] = 1; a.', include: Integer)
assert_call('def f(&); a, b[&] = 1; a.', include: Integer)
assert_call('def f(**); a, b[**] = 1; a.', include: Integer)
# incomplete massign
assert_analyze_type('a,b', :lvar_or_method, 'b')
assert_call('(a=1).b, a.a', include: Integer)
assert_call('(a=1).b, a.', include: Integer)
assert_call('a=1; *a.', include: Integer)
end
Expand Down Expand Up @@ -575,6 +582,7 @@ def test_for
assert_call('for *,(*) in [1,2,3]; 1.', include: Integer)
assert_call('for *i in [1,2,3]; i.sample.', include: Integer)
assert_call('for (a=1).b in [1,2,3]; a.', include: Integer)
assert_call('for a[b=1] in [1,2,3]; b.', include: Integer)
assert_call('for Array::B in [1,2,3]; Array::B.', include: Integer)
assert_call('for A in [1,2,3]; A.', include: Integer)
assert_call('for $a in [1,2,3]; $a.', include: Integer)
Expand Down Expand Up @@ -673,6 +681,7 @@ def test_call_parameter

def test_block_args
assert_call('[1,2,3].tap{|a| a.', include: Array)
assert_call('[1,2,3].tap{|a,| a.', include: Integer)
assert_call('[1,2,3].tap{|a,b| a.', include: Integer)
assert_call('[1,2,3].tap{|(a,b)| a.', include: Integer)
assert_call('[1,2,3].tap{|a,*b| b.', include: Array)
Expand Down