Skip to content

Commit 2363e03

Browse files
committed
Complete keyword arguments
1 parent 6d0a9d1 commit 2363e03

File tree

6 files changed

+100
-19
lines changed

6 files changed

+100
-19
lines changed

lib/repl_type_completor.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
6767
calculate_scope = -> { TypeAnalyzer.calculate_target_type_scope(binding, parents, target_node).last }
6868
calculate_type_scope = ->(node) { TypeAnalyzer.calculate_target_type_scope binding, [*parents, target_node], node }
6969

70+
calculate_lvar_or_method = ->(name) {
71+
if parents[-1].is_a?(Prism::ArgumentsNode) && parents[-2].is_a?(Prism::CallNode)
72+
kwarg_call_node = parents[-2]
73+
kwarg_method_sym = kwarg_call_node.message.to_sym
74+
end
75+
kwarg_call_receiver = nil
76+
lvar_or_method_scope = TypeAnalyzer.calculate_target_type_scope binding, parents, target_node do |dig_targets|
77+
if kwarg_call_node&.receiver
78+
dig_targets.on kwarg_call_node.receiver do |type, _scope|
79+
kwarg_call_receiver = type
80+
end
81+
end
82+
end.last
83+
kwarg_call_receiver = lvar_or_method_scope.self_type if kwarg_call_node && kwarg_call_node.receiver.nil?
84+
[:lvar_or_method, name, lvar_or_method_scope, kwarg_call_receiver && [kwarg_call_receiver, kwarg_method_sym]]
85+
}
86+
7087
case target_node
7188
when Prism::StringNode, Prism::InterpolatedStringNode
7289
call_node, args_node = parents.last(2)
@@ -90,15 +107,15 @@ def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
90107
end
91108
when Prism::CallNode
92109
name = target_node.message.to_s
93-
return [:lvar_or_method, name, calculate_scope.call] if target_node.receiver.nil?
110+
return calculate_lvar_or_method.call(name) if target_node.receiver.nil?
94111

95112
self_call = target_node.receiver.is_a? Prism::SelfNode
96113
op = target_node.call_operator
97114
receiver_type, _scope = calculate_type_scope.call target_node.receiver
98115
receiver_type = receiver_type.nonnillable if op == '&.'
99116
[op == '::' ? :call_or_const : :call, name, receiver_type, self_call]
100117
when Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode
101-
[:lvar_or_method, target_node.name.to_s, calculate_scope.call]
118+
calculate_lvar_or_method.call(target_node.name.to_s)
102119
when Prism::ConstantReadNode, Prism::ConstantTargetNode
103120
name = target_node.name.to_s
104121
if parents.last.is_a? Prism::ConstantPathNode

lib/repl_type_completor/result.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ def completion_candidates
6161
Symbol.all_symbols.map { _1.inspect[1..] }
6262
in [:call, name, type, self_call]
6363
(self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS
64-
in [:lvar_or_method, name, scope]
65-
scope.self_type.all_methods.map(&:to_s) | scope.local_variables | RESERVED_WORDS
64+
in [:lvar_or_method, name, scope, kwarg_call]
65+
kwargs = kwarg_call ? Types.method_kwargs_names(*kwarg_call).map { "#{_1}:" } : []
66+
scope.self_type.all_methods.map(&:to_s) | scope.local_variables | kwargs | RESERVED_WORDS
6667
else
6768
[]
6869
end
@@ -93,7 +94,7 @@ def doc_namespace(matched)
9394
value_doc scope[prefix + matched]
9495
in [:call, prefix, type, _self_call]
9596
method_doc type, prefix + matched
96-
in [:lvar_or_method, prefix, scope]
97+
in [:lvar_or_method, prefix, scope, kwarg_call]
9798
if scope.local_variables.include?(prefix + matched)
9899
value_doc scope[prefix + matched]
99100
else

lib/repl_type_completor/type_analyzer.rb

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
module ReplTypeCompletor
99
class TypeAnalyzer
1010
class DigTarget
11-
def initialize(parents, receiver, &block)
12-
@dig_ids = parents.to_h { [_1.__id__, true] }
13-
@target_id = receiver.__id__
14-
@block = block
11+
def initialize(parents)
12+
@dig_ids = Set.new(parents.map(&:__id__))
13+
@events = {}
1514
end
1615

17-
def dig?(node) = @dig_ids[node.__id__]
18-
def target?(node) = @target_id == node.__id__
19-
def resolve(type, scope)
20-
@block.call type, scope
16+
def on(target, &block)
17+
@dig_ids << target.__id__
18+
@events[target.__id__] = block
19+
end
20+
21+
def dig?(node) = @dig_ids.include?(node.__id__)
22+
def target?(node) = @events.key?(node.__id__)
23+
def trigger(node, type, scope)
24+
@events[node.__id__]&.call type, scope
2125
end
2226
end
2327

@@ -46,7 +50,7 @@ def evaluate(node, scope)
4650
else
4751
result = Types::NIL
4852
end
49-
@dig_targets.resolve result, scope if @dig_targets.target? node
53+
@dig_targets.trigger node, result, scope
5054
result
5155
end
5256

@@ -242,7 +246,7 @@ def evaluate_call_node(node, scope)
242246
# method(args, &:completion_target)
243247
call_block_proc = ->(block_args, _self_type) do
244248
block_receiver = block_args.first || Types::OBJECT
245-
@dig_targets.resolve block_receiver, scope
249+
@dig_targets.trigger block_sym_node, block_receiver, scope
246250
Types::OBJECT
247251
end
248252
else
@@ -892,7 +896,7 @@ def evaluate_constant_node_info(node, scope)
892896
name = node.name.to_s
893897
type = scope[name]
894898
end
895-
@dig_targets.resolve type, scope if @dig_targets.target? node
899+
@dig_targets.trigger node, type, scope
896900
[type, receiver, parent_module, name]
897901
end
898902

@@ -1167,9 +1171,11 @@ def method_call(receiver, method_name, args, kwargs, block, scope, name_match: t
11671171
end
11681172

11691173
def self.calculate_target_type_scope(binding, parents, target)
1170-
dig_targets = DigTarget.new(parents, target) do |type, scope|
1174+
dig_targets = DigTarget.new(parents)
1175+
dig_targets.on target do |type, scope|
11711176
return type, scope
11721177
end
1178+
yield dig_targets if block_given?
11731179
program = parents.first
11741180
scope = Scope.from_binding(binding, program.locals)
11751181
new(dig_targets).evaluate program, scope

lib/repl_type_completor/types.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,39 @@ def self.method_return_type(type, method_name)
6161
types = receivers.flat_map do |receiver_type, klass, singleton|
6262
method = rbs_search_method klass, method_name, singleton
6363
next [] unless method
64-
method.method_types.map do |method|
65-
from_rbs_type(method.type.return_type, receiver_type, {})
64+
method.method_types.map do |method_type|
65+
from_rbs_type(method_type.type.return_type, receiver_type, {})
6666
end
6767
end
6868
UnionType[*types]
6969
end
7070

71+
def self.method_kwargs_names(type, method_name)
72+
receivers = type.types.map do |t|
73+
case t
74+
in SingletonType
75+
[t.module_or_class, true]
76+
in InstanceType
77+
[t.klass, false]
78+
end
79+
end
80+
parameters_keywords = receivers.flat_map do |klass, singleton|
81+
method_obj = singleton ? klass.method(method_name) : klass.instance_method(method_name)
82+
method_obj.parameters.filter_map { _2 if _1 == :key || _1 == :keyreq }
83+
rescue NameError
84+
[]
85+
end
86+
rbs_keywords = receivers.flat_map do |klass, singleton|
87+
method = rbs_search_method klass, method_name, singleton
88+
next [] unless method
89+
90+
method.method_types.flat_map do |method_type|
91+
method_type.type.required_keywords.keys | method_type.type.optional_keywords.keys
92+
end
93+
end
94+
(parameters_keywords | rbs_keywords).sort
95+
end
96+
7197
def self.rbs_methods(type, method_name, args_types, kwargs_type, has_block)
7298
return [] unless rbs_builder
7399

test/repl_type_completor/test_repl_type_completor.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ def test_lvar
7676
assert_doc_namespace('lvar = ""; lvar.ascii_only?', 'String#ascii_only?', binding: bind)
7777
end
7878

79+
def test_kwarg
80+
o = Object.new; def o.foo(bar:, baz: true); end
81+
m = Module.new; def m.foo(foobar:, foobaz: true); end
82+
bind = binding
83+
# kwarg name from method.parameters
84+
assert_completion('o.foo ba', binding: bind, include: ['r:', 'z:'])
85+
assert_completion('m.foo fo', binding: bind, include: ['obar:', 'obaz:'])
86+
assert_completion('foo ba', binding: o.instance_eval { binding }, include: ['r:', 'z:'])
87+
assert_completion('foo fo', binding: m.instance_eval { binding }, include: ['obar:', 'obaz:'])
88+
# kwarg name from RBS
89+
assert_completion('"".each_line ch', binding: bind, include: 'omp:')
90+
assert_completion('String.new en', binding: bind, include: 'coding:')
91+
# assert completion when kwarg name is not found
92+
assert_completion('o.inspect ra', binding: bind, include: 'nd')
93+
assert_completion('o.undefined_method ra', binding: bind, include: 'nd')
94+
end
95+
7996
def test_const
8097
assert_completion('Ar', include: 'ray')
8198
assert_completion('::Ar', include: 'ray')

test/repl_type_completor/test_types.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,19 @@ def bo.foobar; end
8282
type = ReplTypeCompletor::Types.type_from_object bo
8383
assert type.all_methods.include?(:foobar)
8484
end
85+
86+
def test_kwargs_names
87+
bo = BasicObject.new
88+
def bo.foobar(bo_kwarg1: nil, bo_kwarg2:); end
89+
bo_type = ReplTypeCompletor::Types.type_from_object bo
90+
assert_equal %i[bo_kwarg1 bo_kwarg2], ReplTypeCompletor::Types.method_kwargs_names(bo_type, :foobar)
91+
str_type = ReplTypeCompletor::Types::STRING
92+
assert_include ReplTypeCompletor::Types.method_kwargs_names(str_type, :each_line), :chomp
93+
singleton_type = ReplTypeCompletor::Types::SingletonType.new String
94+
assert_include ReplTypeCompletor::Types.method_kwargs_names(singleton_type, :new), :encoding
95+
union_type = ReplTypeCompletor::Types::UnionType[bo_type, str_type, singleton_type]
96+
assert_include ReplTypeCompletor::Types.method_kwargs_names(union_type, :each_line), :chomp
97+
assert_equal ReplTypeCompletor::Types.method_kwargs_names(str_type, :undefined_method), []
98+
end
8599
end
86100
end

0 commit comments

Comments
 (0)