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.
- checksums.yaml +4 -4
- data/.mcp.json +9 -0
- data/README.md +41 -0
- data/exe/type-guessr +30 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +17 -41
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +335 -0
- data/lib/ruby_lsp/type_guessr/config.rb +17 -32
- data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
- data/lib/ruby_lsp/type_guessr/debug_server.rb +18 -15
- data/lib/ruby_lsp/type_guessr/graph_builder.rb +24 -19
- data/lib/ruby_lsp/type_guessr/hover.rb +101 -239
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +540 -278
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
- data/lib/type-guessr.rb +4 -1
- data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +97 -0
- data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +749 -535
- data/lib/type_guessr/core/converter/rbs_converter.rb +20 -13
- data/lib/type_guessr/core/index/location_index.rb +32 -0
- data/lib/type_guessr/core/inference/resolver.rb +385 -216
- data/lib/type_guessr/core/ir/nodes.rb +362 -103
- data/lib/type_guessr/core/logger.rb +3 -8
- data/lib/type_guessr/core/node_context_helper.rb +126 -0
- data/lib/type_guessr/core/node_key_generator.rb +31 -0
- data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
- data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
- data/lib/type_guessr/core/registry/method_registry.rb +56 -38
- data/lib/type_guessr/core/registry/signature_registry.rb +486 -0
- data/lib/type_guessr/core/signature_builder.rb +39 -0
- data/lib/type_guessr/core/type_serializer.rb +92 -0
- data/lib/type_guessr/core/type_simplifier.rb +12 -9
- data/lib/type_guessr/core/types.rb +192 -16
- data/lib/type_guessr/mcp/file_watcher.rb +87 -0
- data/lib/type_guessr/mcp/server.rb +454 -0
- data/lib/type_guessr/mcp/standalone_runtime.rb +253 -0
- data/lib/type_guessr/version.rb +1 -1
- metadata +34 -5
- data/lib/type_guessr/core/rbs_provider.rb +0 -304
- data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fde9ec92176bfee761b076af417d5bd417720c0535f1171ed39005a4dfb7ff5f
|
|
4
|
+
data.tar.gz: 6c99963862f3304fc4e616c8d823a6df9defd0d5d4a71176d2b5fecee696d14c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bbc08313fbdf2e00d4bcf07bc7208534db02ee4500d3af7ca0fc9f2cb2d501f8365944d8aba549cb4a96112a3217a7d145ee6a6c7e8f21fd2c5b0cde7b5c1902
|
|
7
|
+
data.tar.gz: 2b0cff3ce05f061d4ff6c6d48da8e34e6ddc5cc60fd7ecfbfc8a455f7ff83551ece15814010329361a7ef13dd4433526ecda84e750db9907f3b36cc21d99ead1
|
data/.mcp.json
ADDED
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
|
-
#
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
load_config.fetch("
|
|
33
|
+
module_function def background_gem_indexing?
|
|
34
|
+
load_config.fetch("background_gem_indexing", false) == true
|
|
46
35
|
end
|
|
47
36
|
|
|
48
|
-
def
|
|
49
|
-
load_config.fetch("
|
|
37
|
+
module_function def debug_server_port
|
|
38
|
+
load_config.fetch("debug_server_port", 7010)
|
|
50
39
|
end
|
|
51
40
|
|
|
52
|
-
def
|
|
53
|
-
load_config.fetch("
|
|
41
|
+
module_function def gem_inference_timeout
|
|
42
|
+
load_config.fetch("gem_inference_timeout", 1.0)
|
|
54
43
|
end
|
|
55
44
|
|
|
56
|
-
def
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|