|
| 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