Skip to content

Commit d40f9e2

Browse files
committed
Add completor using prism and rbs
1 parent e26e90e commit d40f9e2

File tree

4 files changed

+2283
-0
lines changed

4 files changed

+2283
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
require 'prism'
2+
require 'irb/completion'
3+
require_relative 'type_analyzer'
4+
module IRB
5+
module TypeCompletion
6+
class Completor < BaseCompletor # :nodoc:
7+
HIDDEN_METHODS = %w[Namespace TypeName] # defined by rbs, should be hidden
8+
9+
def inspect
10+
name = 'TypeCompletion::Completor'
11+
if Types.rbs_builder
12+
"#{name} with RBS"
13+
elsif Types.rbs_load_error
14+
"#{name} (no RBS, #{Types.rbs_load_error.inspect})"
15+
else
16+
"#{name} (RBS not loaded)"
17+
end
18+
end
19+
20+
def completion_candidates(preposing, target, _postposing, bind:)
21+
@preposing = preposing
22+
verbose, $VERBOSE = $VERBOSE, nil
23+
code = "#{preposing}#{target}"
24+
@result = analyze code, bind
25+
name, candidates = candidates_from_result(@result)
26+
27+
all_symbols_pattern = /\A[ -\/:-@\[-`\{-~]*\z/
28+
candidates.map(&:to_s).select { !_1.match?(all_symbols_pattern) && _1.start_with?(name) }.uniq.sort.map do
29+
target + _1[name.size..]
30+
end
31+
rescue SyntaxError, StandardError => e
32+
handle_error(e)
33+
[]
34+
ensure
35+
$VERBOSE = verbose
36+
end
37+
38+
def doc_namespace(preposing, matched, postposing, bind:)
39+
name = matched[/[a-zA-Z_0-9]*[!?=]?\z/]
40+
method_doc = -> type do
41+
type = type.types.find { _1.all_methods.include? name.to_sym }
42+
if type.is_a? Types::SingletonType
43+
"#{Types.class_name_of(type.module_or_class)}.#{name}"
44+
elsif type.is_a? Types::InstanceType
45+
"#{Types.class_name_of(type.klass)}##{name}"
46+
end
47+
end
48+
call_or_const_doc = -> type do
49+
if name =~ /\A[A-Z]/
50+
type = type.types.grep(Types::SingletonType).find { _1.module_or_class.const_defined?(name) }
51+
type.module_or_class == Object ? name : "#{Types.class_name_of(type.module_or_class)}::#{name}" if type
52+
else
53+
method_doc.call(type)
54+
end
55+
end
56+
57+
value_doc = -> type do
58+
return unless type
59+
type.types.each do |t|
60+
case t
61+
when Types::SingletonType
62+
return Types.class_name_of(t.module_or_class)
63+
when Types::InstanceType
64+
return Types.class_name_of(t.klass)
65+
when Types::ProcType
66+
return 'Proc'
67+
end
68+
end
69+
nil
70+
end
71+
72+
case @result
73+
in [:call_or_const, type, _name, _self_call]
74+
call_or_const_doc.call type
75+
in [:const, type, _name, scope]
76+
if type
77+
call_or_const_doc.call type
78+
else
79+
value_doc.call scope[name]
80+
end
81+
in [:gvar, _name, scope]
82+
value_doc.call scope["$#{name}"]
83+
in [:ivar, _name, scope]
84+
value_doc.call scope["@#{name}"]
85+
in [:cvar, _name, scope]
86+
value_doc.call scope["@@#{name}"]
87+
in [:call, type, _name, _self_call]
88+
method_doc.call type
89+
in [:lvar_or_method, _name, scope]
90+
if scope.local_variables.include?(name)
91+
value_doc.call scope[name]
92+
else
93+
method_doc.call scope.self_type
94+
end
95+
else
96+
end
97+
end
98+
99+
def candidates_from_result(result)
100+
candidates = case result
101+
in [:require | :require_relative => method, name]
102+
if IRB.const_defined? :RegexpCompletor # IRB::VERSION >= 1.8.2
103+
path_completor = IRB::RegexpCompletor.new
104+
elsif IRB.const_defined? :InputCompletor # IRB::VERSION <= 1.8.1
105+
path_completor = IRB::InputCompletor
106+
end
107+
if !path_completor
108+
[]
109+
elsif method == :require
110+
path_completor.retrieve_files_to_require_from_load_path
111+
else
112+
path_completor.retrieve_files_to_require_relative_from_current_dir
113+
end
114+
in [:call_or_const, type, name, self_call]
115+
((self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS) | type.constants
116+
in [:const, type, name, scope]
117+
if type
118+
scope_constants = type.types.flat_map do |t|
119+
scope.table_module_constants(t.module_or_class) if t.is_a?(Types::SingletonType)
120+
end
121+
(scope_constants.compact | type.constants.map(&:to_s)).sort
122+
else
123+
scope.constants.sort
124+
end
125+
in [:ivar, name, scope]
126+
ivars = scope.instance_variables.sort
127+
name == '@' ? ivars + scope.class_variables.sort : ivars
128+
in [:cvar, name, scope]
129+
scope.class_variables
130+
in [:gvar, name, scope]
131+
scope.global_variables
132+
in [:symbol, name]
133+
Symbol.all_symbols.map { _1.inspect[1..] }
134+
in [:call, type, name, self_call]
135+
(self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS
136+
in [:lvar_or_method, name, scope]
137+
scope.self_type.all_methods.map(&:to_s) | scope.local_variables
138+
else
139+
[]
140+
end
141+
[name || '', candidates]
142+
end
143+
144+
def analyze(code, binding = Object::TOPLEVEL_BINDING)
145+
# Workaround for https://github.com/ruby/prism/issues/1592
146+
return if code.match?(/%[qQ]\z/)
147+
148+
lvars_code = binding.local_variables.map do |name|
149+
"#{name}="
150+
end.join + "nil;\n"
151+
code = lvars_code + code
152+
ast = Prism.parse(code).value
153+
name = code[/(@@|@|\$)?\w*[!?=]?\z/]
154+
*parents, target_node = find_target ast, code.bytesize - name.bytesize
155+
return unless target_node
156+
157+
calculate_scope = -> { TypeAnalyzer.calculate_target_type_scope(binding, parents, target_node).last }
158+
calculate_type_scope = ->(node) { TypeAnalyzer.calculate_target_type_scope binding, [*parents, target_node], node }
159+
160+
if target_node.is_a?(Prism::StringNode) || target_node.is_a?(Prism::InterpolatedStringNode)
161+
args_node = parents[-1]
162+
call_node = parents[-2]
163+
return unless args_node.is_a?(Prism::ArgumentsNode) && args_node.arguments.size == 1
164+
return unless call_node.is_a?(Prism::CallNode) && call_node.receiver.nil? && (call_node.message == 'require' || call_node.message == 'require_relative')
165+
return [call_node.message.to_sym, name.rstrip]
166+
end
167+
168+
case target_node
169+
when Prism::SymbolNode
170+
if parents.last.is_a? Prism::BlockArgumentNode # method(&:target)
171+
receiver_type, _scope = calculate_type_scope.call target_node
172+
[:call, receiver_type, name, false]
173+
else
174+
[:symbol, name] unless name.empty?
175+
end
176+
when Prism::CallNode
177+
return [:lvar_or_method, name, calculate_scope.call] if target_node.receiver.nil?
178+
179+
self_call = target_node.receiver.is_a? Prism::SelfNode
180+
op = target_node.call_operator
181+
receiver_type, _scope = calculate_type_scope.call target_node.receiver
182+
receiver_type = receiver_type.nonnillable if op == '&.'
183+
[op == '::' ? :call_or_const : :call, receiver_type, name, self_call]
184+
when Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode
185+
[:lvar_or_method, name, calculate_scope.call]
186+
when Prism::ConstantReadNode, Prism::ConstantTargetNode
187+
if parents.last.is_a? Prism::ConstantPathNode
188+
path_node = parents.last
189+
if path_node.parent # A::B
190+
receiver, scope = calculate_type_scope.call(path_node.parent)
191+
[:const, receiver, name, scope]
192+
else # ::A
193+
scope = calculate_scope.call
194+
[:const, Types::SingletonType.new(Object), name, scope]
195+
end
196+
else
197+
[:const, nil, name, calculate_scope.call]
198+
end
199+
when Prism::GlobalVariableReadNode, Prism::GlobalVariableTargetNode
200+
[:gvar, name, calculate_scope.call]
201+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableTargetNode
202+
[:ivar, name, calculate_scope.call]
203+
when Prism::ClassVariableReadNode, Prism::ClassVariableTargetNode
204+
[:cvar, name, calculate_scope.call]
205+
end
206+
end
207+
208+
def find_target(node, position)
209+
location = (
210+
case node
211+
when Prism::CallNode
212+
node.message_loc
213+
when Prism::SymbolNode
214+
node.value_loc
215+
when Prism::StringNode
216+
node.content_loc
217+
when Prism::InterpolatedStringNode
218+
node.closing_loc if node.parts.empty?
219+
end
220+
)
221+
return [node] if location&.start_offset == position
222+
223+
node.child_nodes.each do |n|
224+
next unless n.is_a? Prism::Node
225+
match = find_target(n, position)
226+
next unless match
227+
match.unshift node
228+
return match
229+
end
230+
231+
[node] if node.location.start_offset == position
232+
end
233+
234+
def handle_error(e)
235+
end
236+
end
237+
end
238+
end

0 commit comments

Comments
 (0)