diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4683201..a73e7aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/lib/repl_type_completor.rb b/lib/repl_type_completor.rb index 4ccc670..64b83ac 100644 --- a/lib/repl_type_completor.rb +++ b/lib/repl_type_completor.rb @@ -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? @@ -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? @@ -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) diff --git a/lib/repl_type_completor/type_analyzer.rb b/lib/repl_type_completor/type_analyzer.rb index f6b16ab..0e70b32 100644 --- a/lib/repl_type_completor/type_analyzer.rb +++ b/lib/repl_type_completor/type_analyzer.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 scope[node.rest.name.to_s] = Types.array_of(*rest) end node.keywords.each do |n| @@ -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 @@ -1038,8 +1032,8 @@ 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 @@ -1047,7 +1041,6 @@ def evaluate_write(node, value, scope, evaluated_receivers) 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 @@ -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 diff --git a/repl_type_completor.gemspec b/repl_type_completor.gemspec index e5f61af..274c3b3 100644 --- a/repl_type_completor.gemspec +++ b/repl_type_completor.gemspec @@ -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 diff --git a/test/repl_type_completor/test_repl_type_completor.rb b/test/repl_type_completor/test_repl_type_completor.rb index ce991fe..c85ffa8 100644 --- a/test/repl_type_completor/test_repl_type_completor.rb +++ b/test/repl_type_completor/test_repl_type_completor.rb @@ -29,6 +29,8 @@ 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 @@ -36,9 +38,9 @@ def test_require 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 diff --git a/test/repl_type_completor/test_type_analyze.rb b/test/repl_type_completor/test_type_analyze.rb index 03c818e..a8f9a58 100644 --- a/test/repl_type_completor/test_type_analyze.rb +++ b/test/repl_type_completor/test_type_analyze.rb @@ -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 @@ -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) @@ -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)