Class: RubyLsp::TypeGuessr::CodeIndexAdapter
- Inherits:
-
Object
- Object
- RubyLsp::TypeGuessr::CodeIndexAdapter
- Defined in:
- lib/ruby_lsp/type_guessr/code_index_adapter.rb
Overview
Adapter wrapping RubyIndexer for Core layer consumption Provides a stable interface isolating RubyIndexer API changes
Constant Summary collapse
- OBJECT_METHOD_NAMES =
Public instance methods of Object (BasicObject + Kernel). Every class inherits these, so they have zero discriminating power for duck-type candidate search and should be excluded.
%w[ ! != !~ == === <=> __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? respond_to_missing? send singleton_class singleton_method singleton_methods tap then to_enum to_s yield_self ].to_set.freeze
Instance Method Summary collapse
-
#ancestors_of(class_name) ⇒ Array<String>
Get linearized ancestor chain for a class.
-
#build_member_index! ⇒ Object
Build reverse index: method_name → [Entry::Member] Also builds @method_classes: method_name → Set including inherited methods.
-
#class_method_owner(class_name, method_name) ⇒ String?
Look up owner of a class method.
-
#constant_kind(constant_name) ⇒ Symbol?
Get kind of a constant.
-
#find_classes_defining_methods(called_methods) ⇒ Array<String>
Find classes that define ALL given methods (intersection) Each element responds to .name and .positional_count (duck typed).
-
#initialize(index) ⇒ CodeIndexAdapter
constructor
A new instance of CodeIndexAdapter.
-
#instance_method_owner(class_name, method_name) ⇒ String?
Look up owner of an instance method.
-
#member_entries_for_file(file_path) ⇒ Array<RubyIndexer::Entry::Member>
Get all member entries indexed for a specific file.
-
#method_definition_file_path(class_name, method_name, singleton: false) ⇒ String?
Look up file path where a method is defined.
-
#refresh_member_index!(file_uri) ⇒ Object
Incrementally update member_index for a single file.
-
#register_method_class(class_name, method_name) ⇒ Object
Inject a custom method into the duck-typing reverse index.
-
#resolve_constant_name(short_name, nesting) ⇒ String?
Resolve a short constant name to its fully qualified name using nesting context.
-
#unregister_method_classes(class_name) ⇒ Object
Remove all entries for a class from the duck-typing reverse index.
Constructor Details
#initialize(index) ⇒ CodeIndexAdapter
Returns a new instance of CodeIndexAdapter.
27 28 29 30 31 32 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 27 def initialize(index) @index = index @member_index = nil # { method_name => [Entry] } @member_index_files = nil # { file_path => [Entry] } for removal @method_classes = nil # { method_name => Set[class_name] } including inherited end |
Instance Method Details
#ancestors_of(class_name) ⇒ Array<String>
Get linearized ancestor chain for a class
162 163 164 165 166 167 168 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 162 def ancestors_of(class_name) return [] unless @index @index.linearized_ancestors_of(class_name) rescue RubyIndexer::Index::NonExistingNamespaceError [] end |
#build_member_index! ⇒ Object
Build reverse index: method_name → [Entry::Member] Also builds @method_classes: method_name → Set including inherited methods. One-time full scan of index entries, called after initial indexing. Uses keys snapshot + point lookups to avoid Hash iteration conflict with concurrent Index#add on the main LSP thread.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 39 def build_member_index! return unless @index mi = Hash.new { |h, k| h[k] = [] } fi = Hash.new { |h, k| h[k] = [] } # owner_name → [method_name] for ancestor expansion owner_methods = Hash.new { |h, k| h[k] = [] } entries_hash = @index.instance_variable_get(:@entries) keys = entries_hash.keys # atomic snapshot under GIL keys.each do |name| (entries_hash[name] || []).each do |entry| next unless entry.is_a?(RubyIndexer::Entry::Member) && entry.owner mi[entry.name] << entry fp = entry.file_path fi[fp] << entry if fp owner_methods[entry.owner.name] << entry.name unless OBJECT_METHOD_NAMES.include?(entry.name) end end @member_index = mi @member_index_files = fi @method_classes = build_method_classes(owner_methods) end |
#class_method_owner(class_name, method_name) ⇒ String?
Look up owner of a class method
189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 189 def class_method_owner(class_name, method_name) return nil unless @index unqualified_name = ::TypeGuessr::Core::IR.extract_last_name(class_name) singleton_name = "#{class_name}::<Class:#{unqualified_name}>" entries = @index.resolve_method(method_name, singleton_name) return nil if entries.nil? || entries.empty? entries.first.owner&.name rescue RubyIndexer::Index::NonExistingNamespaceError nil end |
#constant_kind(constant_name) ⇒ Symbol?
Get kind of a constant
173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 173 def constant_kind(constant_name) return nil unless @index entries = @index[constant_name] return nil if entries.nil? || entries.empty? case entries.first when RubyIndexer::Entry::Class then :class when RubyIndexer::Entry::Module then :module end end |
#find_classes_defining_methods(called_methods) ⇒ Array<String>
Find classes that define ALL given methods (intersection) Each element responds to .name and .positional_count (duck typed)
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 102 def find_classes_defining_methods(called_methods) return [] if called_methods.empty? return [] unless @index # Exclude Object methods — all classes have them, zero discriminating power called_methods = called_methods.reject { |cm| OBJECT_METHOD_NAMES.include?(cm.name.to_s) } return [] if called_methods.empty? # Pivot approach: 1 lookup + (N-1) resolve_method calls # Pick the longest method name as pivot (likely most specific → fewest candidates) pivot = called_methods.max_by { |cm| cm.name.to_s.length } rest = called_methods - [pivot] # Collect candidates: use @method_classes (includes inherited) when available candidates = if @method_classes (@method_classes[pivot.name.to_s] || Set.new).to_a else entries = if @member_index @member_index[pivot.name.to_s] || [] else @index.fuzzy_search(pivot.name.to_s) do |entry| entry.is_a?(RubyIndexer::Entry::Member) && entry.name == pivot.name.to_s end end entries = filter_by_arity(entries, pivot.positional_count, pivot.keywords) if pivot.positional_count entries.filter_map do |entry| entry.owner.name if entry.respond_to?(:owner) && entry.owner end.uniq end return [] if candidates.empty? # When using method_classes, verify pivot arity too (entries-based path already filtered) methods_to_verify = @method_classes ? called_methods : rest return candidates if methods_to_verify.empty? # Verify each candidate has ALL methods via resolve_method # resolve_method walks the ancestor chain, so inherited methods are found result = candidates.select do |class_name| methods_to_verify.all? do |cm| method_entries = @index.resolve_method(cm.name.to_s, class_name) next false if method_entries.nil? || method_entries.empty? next true unless cm.positional_count method_entries.any? { |e| accepts_arity?(e, cm.positional_count, cm.keywords) } rescue RubyIndexer::Index::NonExistingNamespaceError false end end # Prefer non-singleton classes for duck typing. # Singleton classes (e.g., "Foo::<Class:Foo>") represent class objects, # not instances — they match class methods, not instance methods. non_singleton = result.grep_v(/::<Class:[^>]+>\z/) non_singleton.empty? ? result : non_singleton end |
#instance_method_owner(class_name, method_name) ⇒ String?
Look up owner of an instance method
239 240 241 242 243 244 245 246 247 248 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 239 def instance_method_owner(class_name, method_name) return nil unless @index entries = @index.resolve_method(method_name, class_name) return nil if entries.nil? || entries.empty? entries.first.owner&.name rescue RubyIndexer::Index::NonExistingNamespaceError nil end |
#member_entries_for_file(file_path) ⇒ Array<RubyIndexer::Entry::Member>
Get all member entries indexed for a specific file
92 93 94 95 96 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 92 def member_entries_for_file(file_path) return [] unless @member_index_files @member_index_files[file_path] || [] end |
#method_definition_file_path(class_name, method_name, singleton: false) ⇒ String?
Look up file path where a method is defined
220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 220 def method_definition_file_path(class_name, method_name, singleton: false) return nil unless @index target = if singleton uq = ::TypeGuessr::Core::IR.extract_last_name(class_name) "#{class_name}::<Class:#{uq}>" else class_name end entries = @index.resolve_method(method_name, target) entries&.first&.file_path rescue RubyIndexer::Index::NonExistingNamespaceError nil end |
#refresh_member_index!(file_uri) ⇒ Object
Incrementally update member_index for a single file. Must be called AFTER RubyIndexer has re-indexed the file.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 70 def refresh_member_index!(file_uri) return unless @member_index file_path = file_uri.respond_to?(:full_path) ? file_uri.full_path : file_uri.path # Remove old entries old_entries = @member_index_files.delete(file_path) || [] old_entries.each { |entry| @member_index[entry.name]&.delete(entry) } # Add new entries from RubyIndexer new_entries = @index.entries_for(file_uri, RubyIndexer::Entry::Member) || [] new_entries = new_entries.select(&:owner) new_entries.each { |entry| @member_index[entry.name] << entry } @member_index_files[file_path] = new_entries unless new_entries.empty? # Rebuild method_classes from updated member_index rebuild_method_classes! if @method_classes end |
#register_method_class(class_name, method_name) ⇒ Object
Inject a custom method into the duck-typing reverse index. Used by DSL adapters to register framework-generated methods (e.g., AR column accessors).
252 253 254 255 256 257 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 252 def register_method_class(class_name, method_name) return unless @method_classes @method_classes[method_name] ||= Set.new @method_classes[method_name] << class_name end |
#resolve_constant_name(short_name, nesting) ⇒ String?
Resolve a short constant name to its fully qualified name using nesting context
206 207 208 209 210 211 212 213 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 206 def resolve_constant_name(short_name, nesting) return nil unless @index entries = @index.resolve(short_name, nesting) entries&.first&.name rescue StandardError nil end |
#unregister_method_classes(class_name) ⇒ Object
Remove all entries for a class from the duck-typing reverse index. Used when purging DSL registrations (e.g., schema change).
261 262 263 264 265 |
# File 'lib/ruby_lsp/type_guessr/code_index_adapter.rb', line 261 def unregister_method_classes(class_name) return unless @method_classes @method_classes.each_value { |set| set.delete(class_name) } end |