type-guessr 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.mcp.json +9 -0
  3. data/README.md +41 -0
  4. data/exe/type-guessr +30 -0
  5. data/lib/ruby_lsp/type_guessr/addon.rb +17 -41
  6. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +335 -0
  7. data/lib/ruby_lsp/type_guessr/config.rb +17 -32
  8. data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
  9. data/lib/ruby_lsp/type_guessr/debug_server.rb +18 -15
  10. data/lib/ruby_lsp/type_guessr/graph_builder.rb +24 -19
  11. data/lib/ruby_lsp/type_guessr/hover.rb +101 -239
  12. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +540 -278
  13. data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
  14. data/lib/type-guessr.rb +4 -1
  15. data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
  16. data/lib/type_guessr/core/cache/gem_signature_cache.rb +97 -0
  17. data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
  18. data/lib/type_guessr/core/converter/prism_converter.rb +749 -535
  19. data/lib/type_guessr/core/converter/rbs_converter.rb +20 -13
  20. data/lib/type_guessr/core/index/location_index.rb +32 -0
  21. data/lib/type_guessr/core/inference/resolver.rb +385 -216
  22. data/lib/type_guessr/core/ir/nodes.rb +362 -103
  23. data/lib/type_guessr/core/logger.rb +3 -8
  24. data/lib/type_guessr/core/node_context_helper.rb +126 -0
  25. data/lib/type_guessr/core/node_key_generator.rb +31 -0
  26. data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
  27. data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
  28. data/lib/type_guessr/core/registry/method_registry.rb +56 -38
  29. data/lib/type_guessr/core/registry/signature_registry.rb +486 -0
  30. data/lib/type_guessr/core/signature_builder.rb +39 -0
  31. data/lib/type_guessr/core/type_serializer.rb +92 -0
  32. data/lib/type_guessr/core/type_simplifier.rb +12 -9
  33. data/lib/type_guessr/core/types.rb +192 -16
  34. data/lib/type_guessr/mcp/file_watcher.rb +87 -0
  35. data/lib/type_guessr/mcp/server.rb +454 -0
  36. data/lib/type_guessr/mcp/standalone_runtime.rb +253 -0
  37. data/lib/type_guessr/version.rb +1 -1
  38. metadata +34 -5
  39. data/lib/type_guessr/core/rbs_provider.rb +0 -304
  40. data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
  41. data/lib/type_guessr/core/signature_provider.rb +0 -101
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 896566907c498dda0251546023a2d6cd8431cc8fdb5f7cbb72c6ef902067d95a
4
- data.tar.gz: a8104ed21dff5b62e066223b7fe3a9ff4f9f8b49a5ae2296bd9537a86478d07c
3
+ metadata.gz: fde9ec92176bfee761b076af417d5bd417720c0535f1171ed39005a4dfb7ff5f
4
+ data.tar.gz: 6c99963862f3304fc4e616c8d823a6df9defd0d5d4a71176d2b5fecee696d14c
5
5
  SHA512:
6
- metadata.gz: d0823d8578c5d00d9475f1473349b1ec9aeb96e3e025907a00b933b1e4b32f04663e2e1fe26df0977565d224d54eae4c9a355011826bc8ba2b56496a7750dd3a
7
- data.tar.gz: 02000caaf827f2e0297a80ae7296261b4d803e7f49ba8a64d114733dfdea82e079ee5967e9a7c02a311538d095b1fc25bbf3784db4c0a8d5a1be46cac7a5c24a
6
+ metadata.gz: bbc08313fbdf2e00d4bcf07bc7208534db02ee4500d3af7ca0fc9f2cb2d501f8365944d8aba549cb4a96112a3217a7d145ee6a6c7e8f21fd2c5b0cde7b5c1902
7
+ data.tar.gz: 2b0cff3ce05f061d4ff6c6d48da8e34e6ddc5cc60fd7ecfbfc8a455f7ff83551ece15814010329361a7ef13dd4433526ecda84e750db9907f3b36cc21d99ead1
data/.mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "type-guessr": {
4
+ "command": "ruby",
5
+ "args": ["exe/type-guessr", "mcp", "."]
6
+ }
7
+ }
8
+ }
9
+
data/README.md CHANGED
@@ -80,6 +80,47 @@ When enabled, a web server starts at `http://127.0.0.1:<port>` (default port: 70
80
80
 
81
81
  This is useful for understanding how TypeGuessr analyzes your codebase and debugging type inference issues.
82
82
 
83
+ ## MCP Server
84
+
85
+ TypeGuessr can run as a standalone [MCP](https://modelcontextprotocol.io/) server, exposing its type inference engine to AI tools like Claude Code.
86
+
87
+ ### Setup
88
+
89
+ Using Claude Code CLI:
90
+
91
+ ```bash
92
+ claude mcp add type-guessr -- bundle exec ruby /path/to/type-guessr/exe/type-guessr mcp
93
+ ```
94
+
95
+ Or add to your project's `.mcp.json` manually:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "type-guessr": {
101
+ "command": "bundle",
102
+ "args": ["exec", "ruby", "/path/to/type-guessr/exe/type-guessr", "mcp"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ Or run directly:
109
+
110
+ ```bash
111
+ bundle exec ruby exe/type-guessr mcp [project_path]
112
+ ```
113
+
114
+ If `project_path` is omitted, the current directory is used.
115
+
116
+ ### Available Tools
117
+
118
+ | Tool | Description |
119
+ |------|-------------|
120
+ | `infer_type` | Infer the type at a specific file/line/column |
121
+ | `get_method_signature` | Get the inferred signature of a method |
122
+ | `search_methods` | Search for method definitions by name or pattern |
123
+
83
124
  ## Development
84
125
 
85
126
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/exe/type-guessr ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+
6
+ command = ARGV[0]
7
+
8
+ case command
9
+ when "mcp"
10
+ project_path = ARGV[1] || Dir.pwd
11
+ require_relative "../lib/type_guessr/mcp/server"
12
+ server = TypeGuessr::MCP::Server.new(project_path: project_path)
13
+ server.start
14
+ when "version", "--version", "-v"
15
+ require_relative "../lib/type_guessr/version"
16
+ puts "type-guessr #{TypeGuessr::VERSION}"
17
+ when "help", "--help", "-h", nil
18
+ puts <<~USAGE
19
+ Usage: type-guessr <command> [options]
20
+
21
+ Commands:
22
+ mcp [project_path] Start MCP server for the given project (default: current directory)
23
+ version Show version
24
+ help Show this help
25
+ USAGE
26
+ else
27
+ warn "Unknown command: #{command}"
28
+ warn "Run 'type-guessr help' for usage"
29
+ exit 1
30
+ end
@@ -2,43 +2,16 @@
2
2
 
3
3
  require "ruby_lsp/addon"
4
4
  require_relative "config"
5
+ require_relative "constants"
5
6
  require_relative "runtime_adapter"
6
7
  require_relative "hover"
7
8
  require_relative "debug_server"
8
- require_relative "../../type_guessr/core/rbs_provider"
9
9
 
10
10
  module RubyLsp
11
11
  module TypeGuessr
12
12
  # TypeGuessr addon for Ruby LSP
13
13
  # Provides heuristic type inference without requiring type annotations
14
14
  class Addon < ::RubyLsp::Addon
15
- # Node types to add to Ruby LSP's ALLOWED_TARGETS for hover support
16
- HOVER_TARGET_NODES = [
17
- Prism::LocalVariableReadNode,
18
- Prism::LocalVariableWriteNode,
19
- Prism::LocalVariableTargetNode,
20
- Prism::InstanceVariableReadNode,
21
- Prism::InstanceVariableWriteNode,
22
- Prism::InstanceVariableTargetNode,
23
- Prism::ClassVariableReadNode,
24
- Prism::ClassVariableWriteNode,
25
- Prism::ClassVariableTargetNode,
26
- Prism::GlobalVariableReadNode,
27
- Prism::GlobalVariableWriteNode,
28
- Prism::GlobalVariableTargetNode,
29
- Prism::SelfNode,
30
- Prism::RequiredParameterNode,
31
- Prism::OptionalParameterNode,
32
- Prism::RestParameterNode,
33
- Prism::RequiredKeywordParameterNode,
34
- Prism::OptionalKeywordParameterNode,
35
- Prism::KeywordRestParameterNode,
36
- Prism::BlockParameterNode,
37
- Prism::ForwardingParameterNode,
38
- Prism::CallNode,
39
- Prism::DefNode,
40
- ].freeze
41
-
42
15
  attr_reader :runtime_adapter
43
16
 
44
17
  def name
@@ -46,6 +19,14 @@ module RubyLsp
46
19
  end
47
20
 
48
21
  def activate(global_state, message_queue)
22
+ unless Config.enabled?
23
+ message_queue << RubyLsp::Notification.window_log_message(
24
+ "[TypeGuessr] Disabled via config",
25
+ type: RubyLsp::Constant::MessageType::LOG
26
+ )
27
+ return
28
+ end
29
+
49
30
  @global_state = global_state
50
31
  @message_queue = message_queue
51
32
  @runtime_adapter = RuntimeAdapter.new(global_state, message_queue)
@@ -54,10 +35,7 @@ module RubyLsp
54
35
  # Extend Ruby LSP's hover targets to include variables and parameters
55
36
  extend_hover_targets
56
37
 
57
- # Preload RBS environment
58
- ::TypeGuessr::Core::RBSProvider.instance.preload
59
-
60
- # Start background indexing
38
+ # Start indexing (RBS preload, member_index, project files, gem cache)
61
39
  @runtime_adapter.start_indexing
62
40
 
63
41
  # Swap TypeInferrer for enhanced Go to Definition
@@ -79,14 +57,14 @@ module RubyLsp
79
57
  end
80
58
 
81
59
  def create_hover_listener(response_builder, node_context, dispatcher)
82
- return unless Config.enabled?
60
+ return unless @runtime_adapter
83
61
 
84
62
  Hover.new(@runtime_adapter, response_builder, node_context, dispatcher, @global_state)
85
63
  end
86
64
 
87
65
  # Handle file changes
88
66
  def workspace_did_change_watched_files(changes)
89
- return unless Config.enabled?
67
+ return unless @runtime_adapter
90
68
 
91
69
  changes.each do |change|
92
70
  uri = URI(change[:uri])
@@ -98,22 +76,20 @@ module RubyLsp
98
76
  reindex_file(uri)
99
77
  when RubyLsp::Constant::FileChangeType::DELETED
100
78
  file_path = uri.to_standardized_path
101
- @runtime_adapter.instance_variable_get(:@location_index).remove_file(file_path) if file_path
79
+ @runtime_adapter.remove_indexed_file(file_path) if file_path
102
80
  end
103
81
  end
104
82
  end
105
83
 
106
- private
107
-
108
- def extend_hover_targets
84
+ private def extend_hover_targets
109
85
  targets = RubyLsp::Listeners::Hover::ALLOWED_TARGETS
110
86
 
111
- HOVER_TARGET_NODES.each do |target|
87
+ Constants::HOVER_NODE_MAPPING.each_value do |target|
112
88
  targets << target unless targets.include?(target)
113
89
  end
114
90
  end
115
91
 
116
- def reindex_file(uri)
92
+ private def reindex_file(uri)
117
93
  file_path = uri.path
118
94
  return unless file_path && File.exist?(file_path)
119
95
 
@@ -123,7 +99,7 @@ module RubyLsp
123
99
  warn("[TypeGuessr] Error indexing #{uri}: #{e.message}")
124
100
  end
125
101
 
126
- def start_debug_server
102
+ private def start_debug_server
127
103
  port = Config.debug_server_port
128
104
  warn("[TypeGuessr] Starting debug server on port #{port}...")
129
105
  @debug_server = DebugServer.new(@global_state, @runtime_adapter, port: port)
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLsp
4
+ module TypeGuessr
5
+ # Adapter wrapping RubyIndexer for Core layer consumption
6
+ # Provides a stable interface isolating RubyIndexer API changes
7
+ class CodeIndexAdapter
8
+ # Public instance methods of Object (BasicObject + Kernel).
9
+ # Every class inherits these, so they have zero discriminating power
10
+ # for duck-type candidate search and should be excluded.
11
+ OBJECT_METHOD_NAMES = %w[
12
+ ! != !~ == === <=>
13
+ __id__ __send__
14
+ class clone define_singleton_method display dup
15
+ enum_for eql? equal? extend
16
+ freeze frozen? hash inspect
17
+ instance_of? instance_variable_defined?
18
+ instance_variable_get instance_variable_set instance_variables
19
+ is_a? itself kind_of?
20
+ method methods nil? object_id
21
+ private_methods protected_methods public_method public_methods public_send
22
+ remove_instance_variable respond_to? respond_to_missing?
23
+ send singleton_class singleton_method singleton_methods
24
+ tap then to_enum to_s yield_self
25
+ ].to_set.freeze
26
+
27
+ def initialize(index)
28
+ @index = index
29
+ @member_index = nil # { method_name => [Entry] }
30
+ @member_index_files = nil # { file_path => [Entry] } for removal
31
+ @method_classes = nil # { method_name => Set[class_name] } including inherited
32
+ end
33
+
34
+ # Build reverse index: method_name → [Entry::Member]
35
+ # Also builds @method_classes: method_name → Set[class_name] including inherited methods.
36
+ # One-time full scan of index entries, called after initial indexing.
37
+ # Uses keys snapshot + point lookups to avoid Hash iteration conflict
38
+ # with concurrent Index#add on the main LSP thread.
39
+ def build_member_index!
40
+ return unless @index
41
+
42
+ mi = Hash.new { |h, k| h[k] = [] }
43
+ fi = Hash.new { |h, k| h[k] = [] }
44
+ # owner_name → [method_name] for ancestor expansion
45
+ owner_methods = Hash.new { |h, k| h[k] = [] }
46
+
47
+ entries_hash = @index.instance_variable_get(:@entries)
48
+ keys = entries_hash.keys # atomic snapshot under GIL
49
+
50
+ keys.each do |name|
51
+ (entries_hash[name] || []).each do |entry|
52
+ next unless entry.is_a?(RubyIndexer::Entry::Member) && entry.owner
53
+
54
+ mi[entry.name] << entry
55
+ fp = entry.file_path
56
+ fi[fp] << entry if fp
57
+
58
+ owner_methods[entry.owner.name] << entry.name unless OBJECT_METHOD_NAMES.include?(entry.name)
59
+ end
60
+ end
61
+
62
+ @member_index = mi
63
+ @member_index_files = fi
64
+ @method_classes = build_method_classes(owner_methods)
65
+ end
66
+
67
+ # Incrementally update member_index for a single file.
68
+ # Must be called AFTER RubyIndexer has re-indexed the file.
69
+ # @param file_uri [URI::Generic] File URI (same format as RubyIndexer entries)
70
+ def refresh_member_index!(file_uri)
71
+ return unless @member_index
72
+
73
+ file_path = file_uri.respond_to?(:full_path) ? file_uri.full_path : file_uri.path
74
+
75
+ # Remove old entries
76
+ old_entries = @member_index_files.delete(file_path) || []
77
+ old_entries.each { |entry| @member_index[entry.name]&.delete(entry) }
78
+
79
+ # Add new entries from RubyIndexer
80
+ new_entries = @index.entries_for(file_uri, RubyIndexer::Entry::Member) || []
81
+ new_entries = new_entries.select(&:owner)
82
+ new_entries.each { |entry| @member_index[entry.name] << entry }
83
+ @member_index_files[file_path] = new_entries unless new_entries.empty?
84
+
85
+ # Rebuild method_classes from updated member_index
86
+ rebuild_method_classes! if @method_classes
87
+ end
88
+
89
+ # Get all member entries indexed for a specific file
90
+ # @param file_path [String] Absolute file path
91
+ # @return [Array<RubyIndexer::Entry::Member>] Member entries for the file
92
+ def member_entries_for_file(file_path)
93
+ return [] unless @member_index_files
94
+
95
+ @member_index_files[file_path] || []
96
+ end
97
+
98
+ # Find classes that define ALL given methods (intersection)
99
+ # Each element responds to .name and .positional_count (duck typed)
100
+ # @param called_methods [Array<#name, #positional_count>] Methods to search
101
+ # @return [Array<String>] Class names defining all methods
102
+ def find_classes_defining_methods(called_methods)
103
+ return [] if called_methods.empty?
104
+ return [] unless @index
105
+
106
+ # Exclude Object methods — all classes have them, zero discriminating power
107
+ called_methods = called_methods.reject { |cm| OBJECT_METHOD_NAMES.include?(cm.name.to_s) }
108
+ return [] if called_methods.empty?
109
+
110
+ # Pivot approach: 1 lookup + (N-1) resolve_method calls
111
+ # Pick the longest method name as pivot (likely most specific → fewest candidates)
112
+ pivot = called_methods.max_by { |cm| cm.name.to_s.length }
113
+ rest = called_methods - [pivot]
114
+
115
+ # Collect candidates: use @method_classes (includes inherited) when available
116
+ candidates = if @method_classes
117
+ (@method_classes[pivot.name.to_s] || Set.new).to_a
118
+ else
119
+ entries = if @member_index
120
+ @member_index[pivot.name.to_s] || []
121
+ else
122
+ @index.fuzzy_search(pivot.name.to_s) do |entry|
123
+ entry.is_a?(RubyIndexer::Entry::Member) && entry.name == pivot.name.to_s
124
+ end
125
+ end
126
+ entries = filter_by_arity(entries, pivot.positional_count, pivot.keywords) if pivot.positional_count
127
+ entries.filter_map do |entry|
128
+ entry.owner.name if entry.respond_to?(:owner) && entry.owner
129
+ end.uniq
130
+ end
131
+
132
+ return [] if candidates.empty?
133
+
134
+ # When using method_classes, verify pivot arity too (entries-based path already filtered)
135
+ methods_to_verify = @method_classes ? called_methods : rest
136
+ return candidates if methods_to_verify.empty?
137
+
138
+ # Verify each candidate has ALL methods via resolve_method
139
+ # resolve_method walks the ancestor chain, so inherited methods are found
140
+ result = candidates.select do |class_name|
141
+ methods_to_verify.all? do |cm|
142
+ method_entries = @index.resolve_method(cm.name.to_s, class_name)
143
+ next false if method_entries.nil? || method_entries.empty?
144
+ next true unless cm.positional_count
145
+
146
+ method_entries.any? { |e| accepts_arity?(e, cm.positional_count, cm.keywords) }
147
+ rescue RubyIndexer::Index::NonExistingNamespaceError
148
+ false
149
+ end
150
+ end
151
+
152
+ # Prefer non-singleton classes for duck typing.
153
+ # Singleton classes (e.g., "Foo::<Class:Foo>") represent class objects,
154
+ # not instances — they match class methods, not instance methods.
155
+ non_singleton = result.grep_v(/::<Class:[^>]+>\z/)
156
+ non_singleton.empty? ? result : non_singleton
157
+ end
158
+
159
+ # Get linearized ancestor chain for a class
160
+ # @param class_name [String] Fully qualified class name
161
+ # @return [Array<String>] Ancestor names in MRO order
162
+ def ancestors_of(class_name)
163
+ return [] unless @index
164
+
165
+ @index.linearized_ancestors_of(class_name)
166
+ rescue RubyIndexer::Index::NonExistingNamespaceError
167
+ []
168
+ end
169
+
170
+ # Get kind of a constant
171
+ # @param constant_name [String] Fully qualified constant name
172
+ # @return [Symbol, nil] :class, :module, or nil
173
+ def constant_kind(constant_name)
174
+ return nil unless @index
175
+
176
+ entries = @index[constant_name]
177
+ return nil if entries.nil? || entries.empty?
178
+
179
+ case entries.first
180
+ when RubyIndexer::Entry::Class then :class
181
+ when RubyIndexer::Entry::Module then :module
182
+ end
183
+ end
184
+
185
+ # Look up owner of a class method
186
+ # @param class_name [String] Class name
187
+ # @param method_name [String] Method name
188
+ # @return [String, nil] Owner name or nil
189
+ def class_method_owner(class_name, method_name)
190
+ return nil unless @index
191
+
192
+ unqualified_name = ::TypeGuessr::Core::IR.extract_last_name(class_name)
193
+ singleton_name = "#{class_name}::<Class:#{unqualified_name}>"
194
+ entries = @index.resolve_method(method_name, singleton_name)
195
+ return nil if entries.nil? || entries.empty?
196
+
197
+ entries.first.owner&.name
198
+ rescue RubyIndexer::Index::NonExistingNamespaceError
199
+ nil
200
+ end
201
+
202
+ # Resolve a short constant name to its fully qualified name using nesting context
203
+ # @param short_name [String] Short constant name (e.g., "RuntimeAdapter")
204
+ # @param nesting [Array<String>] Nesting context (e.g., ["RubyLsp", "TypeGuessr"])
205
+ # @return [String, nil] Fully qualified name or nil if not found
206
+ def resolve_constant_name(short_name, nesting)
207
+ return nil unless @index
208
+
209
+ entries = @index.resolve(short_name, nesting)
210
+ entries&.first&.name
211
+ rescue StandardError
212
+ nil
213
+ end
214
+
215
+ # Look up file path where a method is defined
216
+ # @param class_name [String] Class name
217
+ # @param method_name [String] Method name
218
+ # @param singleton [Boolean] true for class methods
219
+ # @return [String, nil] File path or nil if not found
220
+ def method_definition_file_path(class_name, method_name, singleton: false)
221
+ return nil unless @index
222
+
223
+ target = if singleton
224
+ uq = ::TypeGuessr::Core::IR.extract_last_name(class_name)
225
+ "#{class_name}::<Class:#{uq}>"
226
+ else
227
+ class_name
228
+ end
229
+ entries = @index.resolve_method(method_name, target)
230
+ entries&.first&.file_path
231
+ rescue RubyIndexer::Index::NonExistingNamespaceError
232
+ nil
233
+ end
234
+
235
+ # Look up owner of an instance method
236
+ # @param class_name [String] Class name
237
+ # @param method_name [String] Method name
238
+ # @return [String, nil] Owner name or nil
239
+ def instance_method_owner(class_name, method_name)
240
+ return nil unless @index
241
+
242
+ entries = @index.resolve_method(method_name, class_name)
243
+ return nil if entries.nil? || entries.empty?
244
+
245
+ entries.first.owner&.name
246
+ rescue RubyIndexer::Index::NonExistingNamespaceError
247
+ nil
248
+ end
249
+
250
+ # Rebuild @method_classes from current @member_index state.
251
+ private def rebuild_method_classes!
252
+ owner_methods = {}
253
+ @member_index.each do |method_name, entries|
254
+ next if OBJECT_METHOD_NAMES.include?(method_name)
255
+
256
+ entries.each do |entry|
257
+ next unless entry.owner
258
+
259
+ (owner_methods[entry.owner.name] ||= []) << method_name
260
+ end
261
+ end
262
+ @method_classes = build_method_classes(owner_methods)
263
+ end
264
+
265
+ # Build method_name → Set[class_name] including inherited methods via ancestor chain.
266
+ # @param owner_methods [Hash{String => Array<String>}] owner_name → [method_names]
267
+ # @return [Hash{String => Set<String>}] method_name → Set[class_name]
268
+ private def build_method_classes(owner_methods)
269
+ mc = Hash.new { |h, k| h[k] = Set.new }
270
+
271
+ # Register direct definitions
272
+ owner_methods.each do |owner_name, method_names|
273
+ method_names.each { |mn| mc[mn] << owner_name }
274
+ end
275
+
276
+ # Expand with inherited: for each class, walk ancestors and register child under ancestor's methods
277
+ # Use .keys snapshot to avoid "can't add a new key during iteration"
278
+ owner_methods.keys.each do |class_name|
279
+ ancestors = @index.linearized_ancestors_of(class_name)
280
+ ancestors.drop(1).each do |ancestor_name|
281
+ next unless owner_methods.key?(ancestor_name)
282
+
283
+ owner_methods[ancestor_name].each do |method_name|
284
+ mc[method_name] << class_name
285
+ end
286
+ end
287
+ rescue RubyIndexer::Index::NonExistingNamespaceError
288
+ next
289
+ end
290
+
291
+ mc
292
+ end
293
+
294
+ private def filter_by_arity(entries, count, keywords)
295
+ entries.select { |entry| accepts_arity?(entry, count, keywords) }
296
+ end
297
+
298
+ private def accepts_arity?(entry, count, keywords)
299
+ sigs = entry.signatures
300
+ # Accessor (reader) has no signatures → accepts 0 arguments only
301
+ return count.zero? && keywords.empty? if sigs.empty?
302
+
303
+ sigs.any? do |sig|
304
+ required = 0
305
+ optional = 0
306
+ has_rest = false
307
+ has_keyword_params = false
308
+
309
+ sig.parameters.each do |p|
310
+ case p
311
+ when RubyIndexer::Entry::RequiredParameter then required += 1
312
+ when RubyIndexer::Entry::OptionalParameter then optional += 1
313
+ when RubyIndexer::Entry::RestParameter then has_rest = true
314
+ when RubyIndexer::Entry::KeywordParameter,
315
+ RubyIndexer::Entry::OptionalKeywordParameter,
316
+ RubyIndexer::Entry::KeywordRestParameter
317
+ has_keyword_params = true
318
+ end
319
+ end
320
+
321
+ # When call has keywords but method has no keyword params,
322
+ # keywords are implicitly converted to a Hash positional argument
323
+ effective_count = count
324
+ effective_count += 1 if keywords.any? && !has_keyword_params
325
+
326
+ if has_rest
327
+ effective_count >= required
328
+ else
329
+ effective_count.between?(required, required + optional)
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
@@ -7,62 +7,49 @@ module RubyLsp
7
7
  # Defaults:
8
8
  # - enabled: true
9
9
  # - debug: false
10
- # - union_cutoff: 10
11
- # - hash_shape_max_fields: 15
12
- # - max_chain_depth: 5
13
10
  module Config
14
11
  CONFIG_FILENAME = ".type-guessr.yml"
15
12
 
16
- # Default values for type inference limits
17
- DEFAULT_UNION_CUTOFF = 10
18
- DEFAULT_HASH_SHAPE_MAX_FIELDS = 15
19
- DEFAULT_MAX_CHAIN_DEPTH = 5
20
-
21
- module_function
22
-
23
- def reset!
13
+ module_function def reset!
24
14
  @cached_config = nil
25
- @cached_mtime = nil
26
15
  end
27
16
 
28
- def enabled?
17
+ module_function def enabled?
29
18
  value = load_config.fetch("enabled", true)
30
19
  value != false
31
20
  end
32
21
 
33
- def debug?
22
+ module_function def debug?
34
23
  load_config["debug"] == true
35
24
  end
36
25
 
37
- def debug_server_enabled?
26
+ module_function def debug_server_enabled?
38
27
  config = load_config
39
28
  return config["debug_server"] if config.key?("debug_server")
40
29
 
41
30
  debug?
42
31
  end
43
32
 
44
- def debug_server_port
45
- load_config.fetch("debug_server_port", 7010)
33
+ module_function def background_gem_indexing?
34
+ load_config.fetch("background_gem_indexing", false) == true
46
35
  end
47
36
 
48
- def union_cutoff
49
- load_config.fetch("union_cutoff", DEFAULT_UNION_CUTOFF)
37
+ module_function def debug_server_port
38
+ load_config.fetch("debug_server_port", 7010)
50
39
  end
51
40
 
52
- def hash_shape_max_fields
53
- load_config.fetch("hash_shape_max_fields", DEFAULT_HASH_SHAPE_MAX_FIELDS)
41
+ module_function def gem_inference_timeout
42
+ load_config.fetch("gem_inference_timeout", 1.0)
54
43
  end
55
44
 
56
- def max_chain_depth
57
- load_config.fetch("max_chain_depth", DEFAULT_MAX_CHAIN_DEPTH)
58
- end
45
+ module_function def load_config
46
+ return @cached_config if @cached_config
59
47
 
60
- def load_config
61
48
  path = File.join(Dir.pwd, CONFIG_FILENAME)
62
- return default_config if !File.exist?(path)
63
-
64
- mtime = File.mtime(path)
65
- return @cached_config if @cached_config && @cached_mtime == mtime
49
+ if !File.exist?(path)
50
+ @cached_config = default_config
51
+ return @cached_config
52
+ end
66
53
 
67
54
  require "yaml"
68
55
 
@@ -71,14 +58,12 @@ module RubyLsp
71
58
  data = {} unless data.is_a?(Hash)
72
59
 
73
60
  @cached_config = default_config.merge(data)
74
- @cached_mtime = mtime
75
- @cached_config
76
61
  rescue StandardError => e
77
62
  warn("[TypeGuessr] Error loading config file: #{e.message}")
78
63
  default_config
79
64
  end
80
65
 
81
- def default_config
66
+ module_function def default_config
82
67
  {
83
68
  "enabled" => true,
84
69
  "debug" => false