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
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "config"
|
|
4
4
|
|
|
5
5
|
module TypeGuessr
|
|
6
6
|
module Core
|
|
7
7
|
# Unified logging interface for TypeGuessr
|
|
8
8
|
# Uses Config.debug? to control output
|
|
9
9
|
module Logger
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Log debug message with optional context
|
|
13
|
-
# @param msg [String] the debug message
|
|
14
|
-
# @param context [Hash] optional context information
|
|
15
|
-
def debug(msg, context = {})
|
|
10
|
+
module_function def debug(msg, context = {})
|
|
16
11
|
return unless debug_enabled?
|
|
17
12
|
|
|
18
13
|
output = "[TypeGuessr:DEBUG] #{msg}"
|
|
@@ -23,20 +18,18 @@ module TypeGuessr
|
|
|
23
18
|
# Log error message with optional exception
|
|
24
19
|
# @param msg [String] the error message
|
|
25
20
|
# @param exception [Exception, nil] optional exception for backtrace
|
|
26
|
-
def error(msg, exception = nil)
|
|
27
|
-
return unless debug_enabled?
|
|
28
|
-
|
|
21
|
+
module_function def error(msg, exception = nil)
|
|
29
22
|
warn "[TypeGuessr:ERROR] #{msg}"
|
|
30
23
|
return unless exception
|
|
31
24
|
|
|
32
25
|
warn " #{exception.class}: #{exception.message}"
|
|
33
|
-
warn exception.backtrace
|
|
26
|
+
warn exception.backtrace&.first(5)&.map { |l| " #{l}" }&.join("\n")
|
|
34
27
|
end
|
|
35
28
|
|
|
36
29
|
# Check if debug mode is enabled
|
|
37
30
|
# @return [Boolean] true if Config.debug? returns true
|
|
38
|
-
def debug_enabled?
|
|
39
|
-
|
|
31
|
+
module_function def debug_enabled?
|
|
32
|
+
TypeGuessr::Core::Config.debug?
|
|
40
33
|
end
|
|
41
34
|
end
|
|
42
35
|
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "node_key_generator"
|
|
4
|
+
|
|
5
|
+
module TypeGuessr
|
|
6
|
+
module Core
|
|
7
|
+
# Helper module for generating scope IDs and node hashes from Prism nodes and NodeContext.
|
|
8
|
+
# Extracted from Hover and TypeInferrer to eliminate code duplication.
|
|
9
|
+
#
|
|
10
|
+
# This module provides framework-agnostic utilities that bridge between ruby-lsp's
|
|
11
|
+
# NodeContext and TypeGuessr's IR node key format.
|
|
12
|
+
module NodeContextHelper
|
|
13
|
+
module_function def generate_scope_id(node_context, exclude_method: false)
|
|
14
|
+
class_path = node_context.nesting.map do |n|
|
|
15
|
+
n.is_a?(String) ? n : n.name.to_s
|
|
16
|
+
end.join("::")
|
|
17
|
+
|
|
18
|
+
method_name = exclude_method ? nil : node_context.surrounding_method
|
|
19
|
+
|
|
20
|
+
if method_name
|
|
21
|
+
"#{class_path}##{method_name}"
|
|
22
|
+
else
|
|
23
|
+
class_path
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Generate node_hash from Prism node to match IR node_hash format
|
|
28
|
+
# @param node [Prism::Node] The Prism node
|
|
29
|
+
# @param node_context [RubyLsp::NodeContext] The context (for block param detection and nesting)
|
|
30
|
+
# @return [String, nil] The node hash or nil for unsupported node types
|
|
31
|
+
module_function def generate_node_hash(node, node_context)
|
|
32
|
+
offset = node.location.start_offset
|
|
33
|
+
case node
|
|
34
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode
|
|
35
|
+
NodeKeyGenerator.local_write(node.name, offset)
|
|
36
|
+
when Prism::LocalVariableReadNode
|
|
37
|
+
NodeKeyGenerator.local_read(node.name, offset)
|
|
38
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableTargetNode
|
|
39
|
+
NodeKeyGenerator.ivar_write(node.name, offset)
|
|
40
|
+
when Prism::InstanceVariableReadNode
|
|
41
|
+
NodeKeyGenerator.ivar_read(node.name, offset)
|
|
42
|
+
when Prism::ClassVariableWriteNode, Prism::ClassVariableTargetNode
|
|
43
|
+
NodeKeyGenerator.cvar_write(node.name, offset)
|
|
44
|
+
when Prism::ClassVariableReadNode
|
|
45
|
+
NodeKeyGenerator.cvar_read(node.name, offset)
|
|
46
|
+
when Prism::GlobalVariableWriteNode, Prism::GlobalVariableTargetNode
|
|
47
|
+
NodeKeyGenerator.global_write(node.name, offset)
|
|
48
|
+
when Prism::GlobalVariableReadNode
|
|
49
|
+
NodeKeyGenerator.global_read(node.name, offset)
|
|
50
|
+
when Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
51
|
+
Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
|
|
52
|
+
Prism::KeywordRestParameterNode, Prism::BlockParameterNode
|
|
53
|
+
# Check if this is a block parameter (parent is BlockParametersNode)
|
|
54
|
+
if block_parameter?(node, node_context)
|
|
55
|
+
index = block_parameter_index(node, node_context)
|
|
56
|
+
NodeKeyGenerator.bparam(index, offset)
|
|
57
|
+
else
|
|
58
|
+
NodeKeyGenerator.param(node.name, offset)
|
|
59
|
+
end
|
|
60
|
+
when Prism::ForwardingParameterNode
|
|
61
|
+
NodeKeyGenerator.param(:"...", offset)
|
|
62
|
+
when Prism::CallNode
|
|
63
|
+
# Use message_loc for accurate offset
|
|
64
|
+
call_offset = node.message_loc&.start_offset || offset
|
|
65
|
+
NodeKeyGenerator.call(node.name, call_offset)
|
|
66
|
+
when Prism::DefNode
|
|
67
|
+
# Use name_loc for accurate offset
|
|
68
|
+
def_offset = node.name_loc&.start_offset || offset
|
|
69
|
+
NodeKeyGenerator.def_node(node.name, def_offset)
|
|
70
|
+
when Prism::SelfNode
|
|
71
|
+
class_path = node_context.nesting.map do |n|
|
|
72
|
+
n.is_a?(String) ? n : n.name.to_s
|
|
73
|
+
end.join("::")
|
|
74
|
+
NodeKeyGenerator.self_node(class_path, offset)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if a parameter node is inside a block (not a method definition)
|
|
79
|
+
# @param node [Prism::Node] The parameter node
|
|
80
|
+
# @param node_context [RubyLsp::NodeContext] The context
|
|
81
|
+
# @return [Boolean] true if inside a block
|
|
82
|
+
module_function def block_parameter?(node, node_context)
|
|
83
|
+
call_node = node_context.call_node
|
|
84
|
+
return false unless call_node&.block
|
|
85
|
+
|
|
86
|
+
# Check if this parameter is in the block's parameters
|
|
87
|
+
block_params = call_node.block.parameters&.parameters
|
|
88
|
+
return false unless block_params
|
|
89
|
+
|
|
90
|
+
all_params = collect_block_params(block_params)
|
|
91
|
+
all_params.include?(node)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get the index of a block parameter
|
|
95
|
+
# @param node [Prism::Node] The parameter node
|
|
96
|
+
# @param node_context [RubyLsp::NodeContext] The context
|
|
97
|
+
# @return [Integer] The parameter index
|
|
98
|
+
module_function def block_parameter_index(node, node_context)
|
|
99
|
+
call_node = node_context.call_node
|
|
100
|
+
return 0 unless call_node&.block
|
|
101
|
+
|
|
102
|
+
block_params = call_node.block.parameters&.parameters
|
|
103
|
+
return 0 unless block_params
|
|
104
|
+
|
|
105
|
+
all_params = collect_block_params(block_params)
|
|
106
|
+
all_params.index(node) || 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Collect all parameters from block parameters node
|
|
110
|
+
# Uses respond_to? guards for safety across different Prism versions
|
|
111
|
+
# @param block_params [Prism::ParametersNode] The block parameters
|
|
112
|
+
# @return [Array<Prism::Node>] All parameter nodes
|
|
113
|
+
module_function def collect_block_params(block_params)
|
|
114
|
+
params = []
|
|
115
|
+
params.concat(block_params.requireds) if block_params.respond_to?(:requireds)
|
|
116
|
+
params.concat(block_params.optionals) if block_params.respond_to?(:optionals)
|
|
117
|
+
params << block_params.rest if block_params.respond_to?(:rest) && block_params.rest
|
|
118
|
+
params.concat(block_params.posts) if block_params.respond_to?(:posts)
|
|
119
|
+
params.concat(block_params.keywords) if block_params.respond_to?(:keywords)
|
|
120
|
+
params << block_params.keyword_rest if block_params.respond_to?(:keyword_rest) && block_params.keyword_rest
|
|
121
|
+
params << block_params.block if block_params.respond_to?(:block) && block_params.block
|
|
122
|
+
params.compact
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
# Generates unique node keys for IR nodes and Prism node lookups.
|
|
6
|
+
# Single source of truth for node key format to ensure consistency
|
|
7
|
+
# between IR node generation and hover/type inference lookups.
|
|
8
|
+
module NodeKeyGenerator
|
|
9
|
+
module_function def local_write(name, offset) = "local_write:#{name}:#{offset}"
|
|
10
|
+
module_function def local_read(name, offset) = "local_read:#{name}:#{offset}"
|
|
11
|
+
module_function def ivar_write(name, offset) = "ivar_write:#{name}:#{offset}"
|
|
12
|
+
module_function def ivar_read(name, offset) = "ivar_read:#{name}:#{offset}"
|
|
13
|
+
module_function def cvar_write(name, offset) = "cvar_write:#{name}:#{offset}"
|
|
14
|
+
module_function def cvar_read(name, offset) = "cvar_read:#{name}:#{offset}"
|
|
15
|
+
module_function def global_write(name, offset) = "global_write:#{name}:#{offset}"
|
|
16
|
+
module_function def global_read(name, offset) = "global_read:#{name}:#{offset}"
|
|
17
|
+
module_function def param(name, offset) = "param:#{name}:#{offset}"
|
|
18
|
+
module_function def bparam(index, offset) = "bparam:#{index}:#{offset}"
|
|
19
|
+
module_function def call(method, offset) = "call:#{method}:#{offset}"
|
|
20
|
+
module_function def def_node(name, offset) = "def:#{name}:#{offset}"
|
|
21
|
+
module_function def self_node(class_name, offset) = "self:#{class_name}:#{offset}"
|
|
22
|
+
module_function def return_node(offset) = "return:#{offset}"
|
|
23
|
+
module_function def merge(offset) = "merge:#{offset}"
|
|
24
|
+
module_function def or_node(offset) = "or:#{offset}"
|
|
25
|
+
module_function def literal(type_name, offset) = "lit:#{type_name}:#{offset}"
|
|
26
|
+
module_function def constant(name, offset) = "const:#{name}:#{offset}"
|
|
27
|
+
module_function def narrow(kind, offset) = "narrow:#{kind}:#{offset}"
|
|
28
|
+
module_function def class_module(name, offset) = "class:#{name}:#{offset}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Registry
|
|
6
|
+
# Stores and retrieves class variable write nodes
|
|
7
|
+
# No inheritance chain traversal (class variables are class-scoped)
|
|
8
|
+
class ClassVariableRegistry
|
|
9
|
+
def initialize
|
|
10
|
+
@variables = {} # { "ClassName" => { :@@name => WriteNode } }
|
|
11
|
+
@file_entries = {} # { file_path => [[class_name, name], ...] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register a class variable write
|
|
15
|
+
# @param class_name [String] Class name
|
|
16
|
+
# @param name [Symbol] Variable name (e.g., :@@count)
|
|
17
|
+
# @param write_node [IR::ClassVariableWriteNode]
|
|
18
|
+
# @param file_path [String, nil] Source file path for tracking
|
|
19
|
+
def register(class_name, name, write_node, file_path: nil)
|
|
20
|
+
return unless class_name
|
|
21
|
+
|
|
22
|
+
@variables[class_name] ||= {}
|
|
23
|
+
return if @variables[class_name].key?(name) # first write wins
|
|
24
|
+
|
|
25
|
+
@variables[class_name][name] = write_node
|
|
26
|
+
|
|
27
|
+
return unless file_path
|
|
28
|
+
|
|
29
|
+
@file_entries[file_path] ||= []
|
|
30
|
+
@file_entries[file_path] << [class_name, name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Remove all entries registered from a specific file
|
|
34
|
+
# @param file_path [String] Source file path
|
|
35
|
+
def remove_file(file_path)
|
|
36
|
+
entries = @file_entries.delete(file_path)
|
|
37
|
+
return unless entries
|
|
38
|
+
|
|
39
|
+
entries.each do |class_name, name|
|
|
40
|
+
@variables[class_name]&.delete(name)
|
|
41
|
+
@variables.delete(class_name) if @variables[class_name] && @variables[class_name].empty?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Look up a class variable write
|
|
46
|
+
# @param class_name [String]
|
|
47
|
+
# @param name [Symbol]
|
|
48
|
+
# @return [IR::ClassVariableWriteNode, nil]
|
|
49
|
+
def lookup(class_name, name)
|
|
50
|
+
return nil unless class_name
|
|
51
|
+
|
|
52
|
+
@variables.dig(class_name, name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear all registered variables
|
|
56
|
+
def clear
|
|
57
|
+
@variables.clear
|
|
58
|
+
@file_entries.clear
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeGuessr
|
|
4
|
+
module Core
|
|
5
|
+
module Registry
|
|
6
|
+
# Stores and retrieves instance variable write nodes
|
|
7
|
+
# Supports inheritance chain traversal when code_index is set
|
|
8
|
+
class InstanceVariableRegistry
|
|
9
|
+
# Adapter for getting class ancestors (must respond to #ancestors_of)
|
|
10
|
+
# @return [#ancestors_of, nil] Adapter that returns array of ancestor names
|
|
11
|
+
attr_accessor :code_index
|
|
12
|
+
|
|
13
|
+
# @param code_index [#ancestors_of, nil] Adapter for inheritance lookup
|
|
14
|
+
def initialize(code_index: nil)
|
|
15
|
+
@variables = {} # { "ClassName" => { :@name => WriteNode } }
|
|
16
|
+
@file_entries = {} # { file_path => [[class_name, name], ...] }
|
|
17
|
+
@code_index = code_index
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Register an instance variable write
|
|
21
|
+
# @param class_name [String] Class name
|
|
22
|
+
# @param name [Symbol] Variable name (e.g., :@recipe)
|
|
23
|
+
# @param write_node [IR::InstanceVariableWriteNode]
|
|
24
|
+
# @param file_path [String, nil] Source file path for tracking
|
|
25
|
+
def register(class_name, name, write_node, file_path: nil)
|
|
26
|
+
return unless class_name
|
|
27
|
+
|
|
28
|
+
@variables[class_name] ||= {}
|
|
29
|
+
return if @variables[class_name].key?(name) # first write wins
|
|
30
|
+
|
|
31
|
+
@variables[class_name][name] = write_node
|
|
32
|
+
|
|
33
|
+
return unless file_path
|
|
34
|
+
|
|
35
|
+
@file_entries[file_path] ||= []
|
|
36
|
+
@file_entries[file_path] << [class_name, name]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Remove all entries registered from a specific file
|
|
40
|
+
# @param file_path [String] Source file path
|
|
41
|
+
def remove_file(file_path)
|
|
42
|
+
entries = @file_entries.delete(file_path)
|
|
43
|
+
return unless entries
|
|
44
|
+
|
|
45
|
+
entries.each do |class_name, name|
|
|
46
|
+
@variables[class_name]&.delete(name)
|
|
47
|
+
@variables.delete(class_name) if @variables[class_name] && @variables[class_name].empty?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Look up an instance variable write (with inheritance chain traversal)
|
|
52
|
+
# @param class_name [String]
|
|
53
|
+
# @param name [Symbol]
|
|
54
|
+
# @return [IR::InstanceVariableWriteNode, nil]
|
|
55
|
+
def lookup(class_name, name)
|
|
56
|
+
return nil unless class_name
|
|
57
|
+
|
|
58
|
+
# Try current class first
|
|
59
|
+
result = @variables.dig(class_name, name)
|
|
60
|
+
return result if result
|
|
61
|
+
|
|
62
|
+
# Traverse ancestor chain if code_index available
|
|
63
|
+
return nil unless @code_index
|
|
64
|
+
|
|
65
|
+
ancestors = @code_index.ancestors_of(class_name)
|
|
66
|
+
ancestors.each do |ancestor_name|
|
|
67
|
+
next if ancestor_name == class_name # Skip self
|
|
68
|
+
|
|
69
|
+
result = @variables.dig(ancestor_name, name)
|
|
70
|
+
return result if result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Clear all registered variables
|
|
77
|
+
def clear
|
|
78
|
+
@variables.clear
|
|
79
|
+
@file_entries.clear
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -4,25 +4,56 @@ module TypeGuessr
|
|
|
4
4
|
module Core
|
|
5
5
|
module Registry
|
|
6
6
|
# Stores and retrieves project method definitions
|
|
7
|
-
# Supports inheritance chain traversal when
|
|
7
|
+
# Supports inheritance chain traversal when code_index is set
|
|
8
8
|
class MethodRegistry
|
|
9
|
-
#
|
|
10
|
-
# @return [
|
|
11
|
-
attr_accessor :
|
|
9
|
+
# Adapter for getting class ancestors (must respond to #ancestors_of)
|
|
10
|
+
# @return [#ancestors_of, nil] Adapter that returns array of ancestor names
|
|
11
|
+
attr_accessor :code_index
|
|
12
12
|
|
|
13
|
-
# @param
|
|
14
|
-
def initialize(
|
|
13
|
+
# @param code_index [#ancestors_of, nil] Adapter for inheritance lookup
|
|
14
|
+
def initialize(code_index: nil)
|
|
15
15
|
@methods = {} # { "ClassName" => { "method_name" => DefNode } }
|
|
16
|
-
@
|
|
16
|
+
@file_entries = {} # { file_path => [[class_name, method_name], ...] }
|
|
17
|
+
@entry_sources = {} # { [class_name, method_name] => Set[file_path, ...] }
|
|
18
|
+
@code_index = code_index
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# Register a method definition
|
|
20
22
|
# @param class_name [String] Class name (empty string for top-level)
|
|
21
23
|
# @param method_name [String] Method name
|
|
22
24
|
# @param def_node [IR::DefNode] Method definition node
|
|
23
|
-
|
|
25
|
+
# @param file_path [String, nil] Source file path for tracking
|
|
26
|
+
def register(class_name, method_name, def_node, file_path: nil)
|
|
24
27
|
@methods[class_name] ||= {}
|
|
25
28
|
@methods[class_name][method_name] = def_node
|
|
29
|
+
|
|
30
|
+
return unless file_path
|
|
31
|
+
|
|
32
|
+
@file_entries[file_path] ||= []
|
|
33
|
+
@file_entries[file_path] << [class_name, method_name]
|
|
34
|
+
key = [class_name, method_name]
|
|
35
|
+
@entry_sources[key] ||= Set.new
|
|
36
|
+
@entry_sources[key] << file_path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Remove all method entries registered from a specific file
|
|
40
|
+
# @param file_path [String] Source file path
|
|
41
|
+
def remove_file(file_path)
|
|
42
|
+
entries = @file_entries.delete(file_path)
|
|
43
|
+
return unless entries
|
|
44
|
+
|
|
45
|
+
entries.each do |class_name, method_name|
|
|
46
|
+
key = [class_name, method_name]
|
|
47
|
+
sources = @entry_sources[key]
|
|
48
|
+
next unless sources
|
|
49
|
+
|
|
50
|
+
sources.delete(file_path)
|
|
51
|
+
next unless sources.empty?
|
|
52
|
+
|
|
53
|
+
@entry_sources.delete(key)
|
|
54
|
+
@methods[class_name]&.delete(method_name)
|
|
55
|
+
@methods.delete(class_name) if @methods[class_name] && @methods[class_name].empty?
|
|
56
|
+
end
|
|
26
57
|
end
|
|
27
58
|
|
|
28
59
|
# Look up a method definition (with inheritance chain traversal)
|
|
@@ -34,10 +65,10 @@ module TypeGuessr
|
|
|
34
65
|
result = @methods.dig(class_name, method_name)
|
|
35
66
|
return result if result
|
|
36
67
|
|
|
37
|
-
# Traverse ancestor chain if
|
|
38
|
-
return nil unless @
|
|
68
|
+
# Traverse ancestor chain if code_index available
|
|
69
|
+
return nil unless @code_index
|
|
39
70
|
|
|
40
|
-
ancestors = @
|
|
71
|
+
ancestors = @code_index.ancestors_of(class_name)
|
|
41
72
|
ancestors.each do |ancestor_name|
|
|
42
73
|
next if ancestor_name == class_name # Skip self
|
|
43
74
|
|
|
@@ -48,12 +79,6 @@ module TypeGuessr
|
|
|
48
79
|
nil
|
|
49
80
|
end
|
|
50
81
|
|
|
51
|
-
# Get all registered class names
|
|
52
|
-
# @return [Array<String>] List of class names (frozen)
|
|
53
|
-
def registered_classes
|
|
54
|
-
@methods.keys.freeze
|
|
55
|
-
end
|
|
56
|
-
|
|
57
82
|
# Get all methods for a specific class (direct methods only)
|
|
58
83
|
# @param class_name [String] Class name
|
|
59
84
|
# @return [Hash<String, IR::DefNode>] Methods hash (frozen)
|
|
@@ -61,6 +86,27 @@ module TypeGuessr
|
|
|
61
86
|
(@methods[class_name] || {}).freeze
|
|
62
87
|
end
|
|
63
88
|
|
|
89
|
+
# Iterate over all registered methods
|
|
90
|
+
# @yield [class_name, method_name, def_node]
|
|
91
|
+
def each_entry(&block)
|
|
92
|
+
return enum_for(:each_entry) unless block
|
|
93
|
+
|
|
94
|
+
@methods.each do |class_name, methods_hash|
|
|
95
|
+
methods_hash.each do |method_name, def_node|
|
|
96
|
+
block.call(class_name, method_name, def_node)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the source file path for a registered method
|
|
102
|
+
# @param class_name [String] Class name
|
|
103
|
+
# @param method_name [String] Method name
|
|
104
|
+
# @return [String, nil] File path or nil
|
|
105
|
+
def source_file_for(class_name, method_name)
|
|
106
|
+
sources = @entry_sources[[class_name, method_name]]
|
|
107
|
+
sources&.first
|
|
108
|
+
end
|
|
109
|
+
|
|
64
110
|
# Search for methods matching a pattern
|
|
65
111
|
# @param pattern [String] Search pattern (partial match on "ClassName#method_name")
|
|
66
112
|
# @return [Array<Array>] Array of [class_name, method_name, def_node]
|
|
@@ -75,30 +121,11 @@ module TypeGuessr
|
|
|
75
121
|
results
|
|
76
122
|
end
|
|
77
123
|
|
|
78
|
-
# Get all methods available on a class (including inherited)
|
|
79
|
-
# @param class_name [String]
|
|
80
|
-
# @return [Set<String>] Method names
|
|
81
|
-
def all_methods_for_class(class_name)
|
|
82
|
-
# Start with directly defined methods
|
|
83
|
-
class_methods = (@methods[class_name]&.keys || []).to_set
|
|
84
|
-
|
|
85
|
-
# Add inherited methods if ancestry_provider is available
|
|
86
|
-
return class_methods unless @ancestry_provider
|
|
87
|
-
|
|
88
|
-
ancestors = @ancestry_provider.call(class_name)
|
|
89
|
-
ancestors.each do |ancestor_name|
|
|
90
|
-
next if ancestor_name == class_name # Skip self
|
|
91
|
-
|
|
92
|
-
ancestor_methods = @methods[ancestor_name]&.keys || []
|
|
93
|
-
class_methods.merge(ancestor_methods)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
class_methods
|
|
97
|
-
end
|
|
98
|
-
|
|
99
124
|
# Clear all registered methods
|
|
100
125
|
def clear
|
|
101
126
|
@methods.clear
|
|
127
|
+
@file_entries.clear
|
|
128
|
+
@entry_sources.clear
|
|
102
129
|
end
|
|
103
130
|
end
|
|
104
131
|
end
|