type-guessr 0.0.1 → 0.0.3
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/README.md +41 -0
- data/exe/type-guessr +30 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
- data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
- data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
- data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
- data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
- data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
- data/lib/type-guessr.rb +3 -11
- data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
- data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
- data/lib/type_guessr/core/converter/call_converter.rb +161 -0
- data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
- data/lib/type_guessr/core/converter/context.rb +144 -0
- data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
- data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
- data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
- data/lib/type_guessr/core/converter/registration.rb +100 -0
- data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
- data/lib/type_guessr/core/converter.rb +4 -0
- data/lib/type_guessr/core/index/location_index.rb +32 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +516 -349
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir/nodes.rb +362 -103
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +6 -13
- 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 +65 -38
- data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/signature_builder.rb +39 -0
- data/lib/type_guessr/core/type_serializer.rb +96 -0
- data/lib/type_guessr/core/type_simplifier.rb +15 -12
- data/lib/type_guessr/core/types.rb +250 -32
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/file_watcher.rb +87 -0
- data/lib/type_guessr/mcp/server.rb +463 -0
- data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
- data/lib/type_guessr/version.rb +1 -1
- metadata +57 -8
- 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
|
@@ -10,7 +10,8 @@ module RubyLsp
|
|
|
10
10
|
# Core layer shortcuts
|
|
11
11
|
Types = ::TypeGuessr::Core::Types
|
|
12
12
|
IR = ::TypeGuessr::Core::IR
|
|
13
|
-
|
|
13
|
+
NodeContextHelper = ::TypeGuessr::Core::NodeContextHelper
|
|
14
|
+
private_constant :Types, :IR, :NodeContextHelper
|
|
14
15
|
|
|
15
16
|
def initialize(index, runtime_adapter)
|
|
16
17
|
super(index)
|
|
@@ -26,14 +27,7 @@ module RubyLsp
|
|
|
26
27
|
guess_type_from_ir(node_context)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
# Attempt to guess type using IR-based inference
|
|
32
|
-
# Returns GuessedType only when type can be resolved
|
|
33
|
-
#
|
|
34
|
-
# @param node_context [RubyLsp::NodeContext] The context of the node
|
|
35
|
-
# @return [GuessedType, nil] The guessed type or nil if unknown
|
|
36
|
-
def guess_type_from_ir(node_context)
|
|
30
|
+
private def guess_type_from_ir(node_context)
|
|
37
31
|
node = node_context.node
|
|
38
32
|
|
|
39
33
|
# For CallNode, we need to infer the receiver's type
|
|
@@ -64,9 +58,9 @@ module RubyLsp
|
|
|
64
58
|
# @param node [Prism::Node] The Prism node
|
|
65
59
|
# @param node_context [RubyLsp::NodeContext] The context of the node
|
|
66
60
|
# @return [TypeGuessr::Core::IR::Node, nil] The IR node or nil
|
|
67
|
-
def find_ir_node(node, node_context)
|
|
68
|
-
scope_id = generate_scope_id(node_context)
|
|
69
|
-
node_hash = generate_node_hash(node, node_context)
|
|
61
|
+
private def find_ir_node(node, node_context)
|
|
62
|
+
scope_id = NodeContextHelper.generate_scope_id(node_context)
|
|
63
|
+
node_hash = NodeContextHelper.generate_node_hash(node, node_context)
|
|
70
64
|
return nil unless node_hash
|
|
71
65
|
|
|
72
66
|
node_key = "#{scope_id}:#{node_hash}"
|
|
@@ -76,7 +70,7 @@ module RubyLsp
|
|
|
76
70
|
# Extract the variable node from a CallNode's receiver
|
|
77
71
|
# @param call_node [Prism::CallNode] The call node
|
|
78
72
|
# @return [Prism::Node, nil] The receiver variable node or nil
|
|
79
|
-
def extract_receiver_variable(call_node)
|
|
73
|
+
private def extract_receiver_variable(call_node)
|
|
80
74
|
receiver = call_node.receiver
|
|
81
75
|
return nil unless receiver
|
|
82
76
|
|
|
@@ -92,7 +86,7 @@ module RubyLsp
|
|
|
92
86
|
# Check if the node is a variable node that we can analyze
|
|
93
87
|
# @param node [Prism::Node] The node to check
|
|
94
88
|
# @return [Boolean] true if the node is a variable node
|
|
95
|
-
def variable_node?(node)
|
|
89
|
+
private def variable_node?(node)
|
|
96
90
|
case node
|
|
97
91
|
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode,
|
|
98
92
|
Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableTargetNode,
|
|
@@ -104,97 +98,6 @@ module RubyLsp
|
|
|
104
98
|
false
|
|
105
99
|
end
|
|
106
100
|
end
|
|
107
|
-
|
|
108
|
-
# Generate scope_id from node_context
|
|
109
|
-
# Format: "ClassName#method_name" or "ClassName" or "#method_name" or ""
|
|
110
|
-
# @param node_context [RubyLsp::NodeContext] The context of the node
|
|
111
|
-
# @return [String] The scope identifier
|
|
112
|
-
def generate_scope_id(node_context)
|
|
113
|
-
class_path = node_context.nesting.map do |n|
|
|
114
|
-
n.is_a?(String) ? n : n.name.to_s
|
|
115
|
-
end.join("::")
|
|
116
|
-
|
|
117
|
-
method_name = node_context.surrounding_method
|
|
118
|
-
|
|
119
|
-
if method_name
|
|
120
|
-
"#{class_path}##{method_name}"
|
|
121
|
-
else
|
|
122
|
-
class_path
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Generate node_hash from Prism node to match IR node_hash format
|
|
127
|
-
# @param node [Prism::Node] The Prism node
|
|
128
|
-
# @param node_context [RubyLsp::NodeContext] The context (for block param detection)
|
|
129
|
-
# @return [String, nil] The node hash or nil
|
|
130
|
-
def generate_node_hash(node, node_context)
|
|
131
|
-
line = node.location.start_line
|
|
132
|
-
case node
|
|
133
|
-
when Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode
|
|
134
|
-
"local_write:#{node.name}:#{line}"
|
|
135
|
-
when Prism::LocalVariableReadNode
|
|
136
|
-
"local_read:#{node.name}:#{line}"
|
|
137
|
-
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableTargetNode
|
|
138
|
-
"ivar_write:#{node.name}:#{line}"
|
|
139
|
-
when Prism::InstanceVariableReadNode
|
|
140
|
-
"ivar_read:#{node.name}:#{line}"
|
|
141
|
-
when Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
142
|
-
Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
|
|
143
|
-
Prism::KeywordRestParameterNode, Prism::BlockParameterNode
|
|
144
|
-
# Check if this is a block parameter
|
|
145
|
-
if block_parameter?(node, node_context)
|
|
146
|
-
index = block_parameter_index(node, node_context)
|
|
147
|
-
"bparam:#{index}:#{line}"
|
|
148
|
-
else
|
|
149
|
-
"param:#{node.name}:#{line}"
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Check if a parameter node is inside a block (not a method definition)
|
|
155
|
-
# @param node [Prism::Node] The parameter node
|
|
156
|
-
# @param node_context [RubyLsp::NodeContext] The context
|
|
157
|
-
# @return [Boolean] true if inside a block
|
|
158
|
-
def block_parameter?(node, node_context)
|
|
159
|
-
call_node = node_context.call_node
|
|
160
|
-
return false unless call_node&.block
|
|
161
|
-
|
|
162
|
-
block_params = call_node.block.parameters&.parameters
|
|
163
|
-
return false unless block_params
|
|
164
|
-
|
|
165
|
-
all_params = collect_block_params(block_params)
|
|
166
|
-
all_params.include?(node)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Get the index of a block parameter
|
|
170
|
-
# @param node [Prism::Node] The parameter node
|
|
171
|
-
# @param node_context [RubyLsp::NodeContext] The context
|
|
172
|
-
# @return [Integer] The parameter index
|
|
173
|
-
def block_parameter_index(node, node_context)
|
|
174
|
-
call_node = node_context.call_node
|
|
175
|
-
return 0 unless call_node&.block
|
|
176
|
-
|
|
177
|
-
block_params = call_node.block.parameters&.parameters
|
|
178
|
-
return 0 unless block_params
|
|
179
|
-
|
|
180
|
-
all_params = collect_block_params(block_params)
|
|
181
|
-
all_params.index(node) || 0
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Collect all parameters from block parameters node
|
|
185
|
-
# @param block_params [Prism::ParametersNode] The block parameters
|
|
186
|
-
# @return [Array<Prism::Node>] All parameter nodes
|
|
187
|
-
def collect_block_params(block_params)
|
|
188
|
-
params = []
|
|
189
|
-
params.concat(block_params.requireds) if block_params.respond_to?(:requireds)
|
|
190
|
-
params.concat(block_params.optionals) if block_params.respond_to?(:optionals)
|
|
191
|
-
params << block_params.rest if block_params.respond_to?(:rest) && block_params.rest
|
|
192
|
-
params.concat(block_params.posts) if block_params.respond_to?(:posts)
|
|
193
|
-
params.concat(block_params.keywords) if block_params.respond_to?(:keywords)
|
|
194
|
-
params << block_params.keyword_rest if block_params.respond_to?(:keyword_rest) && block_params.keyword_rest
|
|
195
|
-
params << block_params.block if block_params.respond_to?(:block) && block_params.block
|
|
196
|
-
params.compact
|
|
197
|
-
end
|
|
198
101
|
end
|
|
199
102
|
end
|
|
200
103
|
end
|
data/lib/type-guessr.rb
CHANGED
|
@@ -8,21 +8,13 @@ end
|
|
|
8
8
|
# Load version
|
|
9
9
|
require_relative "type_guessr/version"
|
|
10
10
|
|
|
11
|
-
# Load core components
|
|
12
|
-
require_relative "type_guessr/core
|
|
13
|
-
require_relative "type_guessr/core/ir/nodes"
|
|
14
|
-
require_relative "type_guessr/core/index/location_index"
|
|
15
|
-
require_relative "type_guessr/core/converter/prism_converter"
|
|
16
|
-
require_relative "type_guessr/core/converter/rbs_converter"
|
|
17
|
-
require_relative "type_guessr/core/inference/result"
|
|
18
|
-
require_relative "type_guessr/core/inference/resolver"
|
|
19
|
-
require_relative "type_guessr/core/rbs_provider"
|
|
20
|
-
require_relative "type_guessr/core/logger"
|
|
11
|
+
# Load core components
|
|
12
|
+
require_relative "type_guessr/core"
|
|
21
13
|
|
|
22
14
|
# Load Ruby LSP integration
|
|
23
15
|
# NOTE: addon.rb is NOT required here - it's auto-discovered by Ruby LSP
|
|
24
16
|
# Requiring it here would cause double activation
|
|
25
|
-
require_relative "ruby_lsp/type_guessr/
|
|
17
|
+
require_relative "ruby_lsp/type_guessr/constants"
|
|
26
18
|
require_relative "ruby_lsp/type_guessr/runtime_adapter"
|
|
27
19
|
require_relative "ruby_lsp/type_guessr/hover"
|
|
28
20
|
require_relative "ruby_lsp/type_guessr/debug_server"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
module Cache
|
|
8
|
+
# Resolves gem dependencies from Gemfile.lock and partitions files by gem.
|
|
9
|
+
# Provides topological ordering for dependency-aware cache building.
|
|
10
|
+
class GemDependencyResolver
|
|
11
|
+
GEM_PATH_PATTERN = %r{/gems/([^/]+)-(\d[^/]*)/}
|
|
12
|
+
|
|
13
|
+
# @param lockfile_path [String] Path to Gemfile.lock
|
|
14
|
+
def initialize(lockfile_path)
|
|
15
|
+
@lockfile_path = lockfile_path
|
|
16
|
+
@lockfile = parse_lockfile
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Partition file paths into gem files and project files
|
|
20
|
+
# @param file_paths [Array<String>] All indexable file paths
|
|
21
|
+
# @return [Hash] { gems: { name => { version:, files:, transitive_deps: {} } }, project_files: [] }
|
|
22
|
+
def partition(file_paths)
|
|
23
|
+
gems = {}
|
|
24
|
+
project_files = []
|
|
25
|
+
|
|
26
|
+
file_paths.each do |path|
|
|
27
|
+
match = path.match(GEM_PATH_PATTERN)
|
|
28
|
+
if match
|
|
29
|
+
gem_name = match[1]
|
|
30
|
+
gem_version = match[2]
|
|
31
|
+
|
|
32
|
+
# Only include gems from the lockfile
|
|
33
|
+
if @lockfile.key?(gem_name)
|
|
34
|
+
gems[gem_name] ||= {
|
|
35
|
+
version: gem_version,
|
|
36
|
+
files: [],
|
|
37
|
+
transitive_deps: resolve_transitive_deps(gem_name)
|
|
38
|
+
}
|
|
39
|
+
gems[gem_name][:files] << path
|
|
40
|
+
else
|
|
41
|
+
project_files << path
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
project_files << path
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
{ gems: gems, project_files: project_files }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Return gem names in topological order (leaves first, roots last)
|
|
52
|
+
# @param gem_names [Array<String>] Gem names to sort
|
|
53
|
+
# @return [Array<String>] Sorted gem names
|
|
54
|
+
def topological_order(gem_names)
|
|
55
|
+
visited = {}
|
|
56
|
+
order = []
|
|
57
|
+
|
|
58
|
+
gem_names.each do |name|
|
|
59
|
+
visit(name, gem_names, visited, order)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
order
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private def parse_lockfile
|
|
66
|
+
return {} unless File.exist?(@lockfile_path)
|
|
67
|
+
|
|
68
|
+
content = File.read(@lockfile_path)
|
|
69
|
+
parser = Bundler::LockfileParser.new(content)
|
|
70
|
+
|
|
71
|
+
parser.specs.to_h do |spec|
|
|
72
|
+
deps = spec.dependencies.map(&:name)
|
|
73
|
+
[spec.name, { version: spec.version.to_s, deps: deps }]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def resolve_transitive_deps(gem_name)
|
|
78
|
+
result = {}
|
|
79
|
+
queue = (@lockfile.dig(gem_name, :deps) || []).dup
|
|
80
|
+
visited = Set.new
|
|
81
|
+
|
|
82
|
+
while (dep_name = queue.shift)
|
|
83
|
+
next if visited.include?(dep_name)
|
|
84
|
+
|
|
85
|
+
visited << dep_name
|
|
86
|
+
|
|
87
|
+
dep_info = @lockfile[dep_name]
|
|
88
|
+
next unless dep_info
|
|
89
|
+
|
|
90
|
+
result[dep_name] = dep_info[:version]
|
|
91
|
+
queue.concat(dep_info[:deps] || [])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def visit(name, valid_names, visited, order)
|
|
98
|
+
return if visited[name]
|
|
99
|
+
|
|
100
|
+
visited[name] = true
|
|
101
|
+
|
|
102
|
+
# Visit dependencies first
|
|
103
|
+
deps = @lockfile.dig(name, :deps) || []
|
|
104
|
+
deps.each do |dep|
|
|
105
|
+
visit(dep, valid_names, visited, order) if valid_names.include?(dep)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
order << name
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest/sha2"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require_relative "../../version"
|
|
7
|
+
|
|
8
|
+
module TypeGuessr
|
|
9
|
+
module Core
|
|
10
|
+
module Cache
|
|
11
|
+
# Manages disk-based cache of gem method signatures.
|
|
12
|
+
# Cache key = gem name + version + hash of transitive dependencies.
|
|
13
|
+
# Files stored at: ~/.cache/type-guessr/gem-signatures/{name}-{version}-{dep_hash}.json
|
|
14
|
+
class GemSignatureCache
|
|
15
|
+
CACHE_FORMAT_VERSION = 2
|
|
16
|
+
|
|
17
|
+
# @param cache_dir [String, nil] Override cache directory (for testing)
|
|
18
|
+
def initialize(cache_dir: nil)
|
|
19
|
+
@cache_dir = cache_dir || default_cache_dir
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if a cached file exists for the given gem
|
|
23
|
+
# @param gem_name [String]
|
|
24
|
+
# @param gem_version [String]
|
|
25
|
+
# @param transitive_deps [Hash{String => String}] { dep_name => dep_version }
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def cached?(gem_name, gem_version, transitive_deps)
|
|
28
|
+
File.exist?(cache_path(gem_name, gem_version, transitive_deps))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Load cached signatures
|
|
32
|
+
# @param gem_name [String]
|
|
33
|
+
# @param gem_version [String]
|
|
34
|
+
# @param transitive_deps [Hash{String => String}]
|
|
35
|
+
# @return [Hash, nil] { "instance_methods" => {...}, "class_methods" => {...} } or nil on failure
|
|
36
|
+
def load(gem_name, gem_version, transitive_deps)
|
|
37
|
+
path = cache_path(gem_name, gem_version, transitive_deps)
|
|
38
|
+
return nil unless File.exist?(path)
|
|
39
|
+
|
|
40
|
+
data = JSON.parse(File.read(path))
|
|
41
|
+
return nil unless data["version"] == CACHE_FORMAT_VERSION
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
"instance_methods" => data["instance_methods"] || {},
|
|
45
|
+
"class_methods" => data["class_methods"] || {},
|
|
46
|
+
"fully_inferred" => data.fetch("fully_inferred", true),
|
|
47
|
+
"inference_timeout" => data.fetch("inference_timeout", false)
|
|
48
|
+
}
|
|
49
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Save signatures to cache
|
|
54
|
+
# @param gem_name [String]
|
|
55
|
+
# @param gem_version [String]
|
|
56
|
+
# @param transitive_deps [Hash{String => String}]
|
|
57
|
+
# @param instance_methods [Hash] { class_name => { method_name => serialized_entry } }
|
|
58
|
+
# @param class_methods [Hash] { class_name => { method_name => serialized_entry } }
|
|
59
|
+
# @param fully_inferred [Boolean] Whether types are fully inferred (false = Unguessed placeholders)
|
|
60
|
+
def save(gem_name, gem_version, transitive_deps,
|
|
61
|
+
instance_methods:, class_methods:, fully_inferred: true, inference_timeout: false)
|
|
62
|
+
path = cache_path(gem_name, gem_version, transitive_deps)
|
|
63
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
64
|
+
|
|
65
|
+
data = {
|
|
66
|
+
"version" => CACHE_FORMAT_VERSION,
|
|
67
|
+
"fully_inferred" => fully_inferred,
|
|
68
|
+
"inference_timeout" => inference_timeout,
|
|
69
|
+
"instance_methods" => instance_methods,
|
|
70
|
+
"class_methods" => class_methods
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
File.write(path, JSON.generate(data))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Delete all cached files
|
|
77
|
+
def clear!
|
|
78
|
+
FileUtils.rm_rf(@cache_dir)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private def default_cache_dir
|
|
82
|
+
xdg_cache = ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache"))
|
|
83
|
+
File.join(xdg_cache, "type-guessr", "gem-signatures", TypeGuessr::VERSION)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private def cache_path(gem_name, gem_version, transitive_deps)
|
|
87
|
+
dep_hash = compute_dep_hash(transitive_deps)
|
|
88
|
+
File.join(@cache_dir, "#{gem_name}-#{gem_version}-#{dep_hash}.json")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private def compute_dep_hash(transitive_deps)
|
|
92
|
+
sorted_pairs = transitive_deps.sort.map { |name, version| "#{name}:#{version}" }.join(",")
|
|
93
|
+
Digest::SHA256.hexdigest("v1:#{sorted_pairs}")[0..5]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type_serializer"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
module Cache
|
|
8
|
+
# Extracts method signatures from indexed gem files.
|
|
9
|
+
# Iterates DefNodes in the method registry and builds serialized signatures.
|
|
10
|
+
class GemSignatureExtractor
|
|
11
|
+
# @param signature_builder [SignatureBuilder]
|
|
12
|
+
# @param method_registry [Registry::MethodRegistry]
|
|
13
|
+
# @param location_index [Index::LocationIndex]
|
|
14
|
+
def initialize(signature_builder:, method_registry:, location_index:)
|
|
15
|
+
@signature_builder = signature_builder
|
|
16
|
+
@method_registry = method_registry
|
|
17
|
+
@location_index = location_index
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Extract all method signatures from indexed gem files
|
|
21
|
+
# @param gem_files [Array<String>] File paths belonging to this gem
|
|
22
|
+
# @param timeout [Float, nil] Max seconds for inference. Returns nil on timeout.
|
|
23
|
+
# @return [Hash, nil] { instance_methods:, class_methods: } or nil on timeout
|
|
24
|
+
def extract(gem_files, timeout: nil)
|
|
25
|
+
gem_def_nodes = collect_def_nodes(gem_files)
|
|
26
|
+
instance_methods = {}
|
|
27
|
+
class_methods = {}
|
|
28
|
+
deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
|
|
29
|
+
check_counter = 0
|
|
30
|
+
|
|
31
|
+
@method_registry.each_entry do |class_name, method_name, def_node|
|
|
32
|
+
next unless gem_def_nodes.include?(def_node)
|
|
33
|
+
|
|
34
|
+
if deadline
|
|
35
|
+
check_counter += 1
|
|
36
|
+
return nil if (check_counter % 100).zero? && Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig = @signature_builder.build_from_def_node(def_node)
|
|
40
|
+
serialized = serialize_signature(sig)
|
|
41
|
+
|
|
42
|
+
if def_node.singleton
|
|
43
|
+
class_methods[class_name] ||= {}
|
|
44
|
+
class_methods[class_name][method_name] = serialized
|
|
45
|
+
else
|
|
46
|
+
instance_methods[class_name] ||= {}
|
|
47
|
+
instance_methods[class_name][method_name] = serialized
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# module_function: also register as class method
|
|
51
|
+
if def_node.module_function
|
|
52
|
+
class_methods[class_name] ||= {}
|
|
53
|
+
class_methods[class_name][method_name] = serialized
|
|
54
|
+
end
|
|
55
|
+
rescue StandardError
|
|
56
|
+
# Skip methods that fail to infer (circular deps, etc.)
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ instance_methods: instance_methods, class_methods: class_methods }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Collect all DefNodes from gem files using the location index
|
|
64
|
+
# @param gem_files [Array<String>] File paths belonging to this gem
|
|
65
|
+
# @return [Set<IR::DefNode>] Set of DefNodes for O(1) membership check
|
|
66
|
+
private def collect_def_nodes(gem_files)
|
|
67
|
+
result = Set.new
|
|
68
|
+
gem_files.each do |file_path|
|
|
69
|
+
@location_index.nodes_for_file(file_path).each do |node|
|
|
70
|
+
result << node if node.is_a?(IR::DefNode)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private def serialize_signature(method_signature)
|
|
77
|
+
{
|
|
78
|
+
"return_type" => TypeSerializer.serialize(method_signature.return_type),
|
|
79
|
+
"params" => method_signature.params.map do |p|
|
|
80
|
+
{ "name" => p.name.to_s, "kind" => p.kind.to_s, "type" => TypeSerializer.serialize(p.type) }
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -1,68 +1,55 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
module
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
5
|
# Loads TypeGuessr settings from .type-guessr.yml in the current working directory.
|
|
6
6
|
#
|
|
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
|