hind 0.1.5 → 0.1.7
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/exe/hind +2 -2
- data/lib/hind/cli.rb +60 -74
- data/lib/hind/lsif/generator.rb +190 -93
- data/lib/hind/lsif/global_state.rb +73 -11
- data/lib/hind/lsif/visitors/declaration_visitor.rb +69 -0
- data/lib/hind/lsif/visitors/reference_visitor.rb +62 -0
- data/lib/hind/lsif.rb +0 -1
- data/lib/hind/version.rb +1 -1
- metadata +4 -3
- data/lib/hind/lsif/visitor.rb +0 -268
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65e6158fb36117206f66c5989febbd5c336f7b8e88aa3b4a241a7d2cac091fa1
|
4
|
+
data.tar.gz: 753d17f81c708b80ebac94b1e17c1cfa2c34e4ed5bc767f9e2daed525b7485ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 649463b8b55e5016d822334667c1e61f6bc4fefa62230a5f7846f2b396f9ad467c8ff848d9637377c4851bda3d7bb068cc1a319a16d06b31a2269384bc6486c9
|
7
|
+
data.tar.gz: 0376aec80b6dbd5eba49244103d093a004e428e284e644b17109010a3175969f0df9eb261edc94fc60d071a8594a925aaa7ffa975af112c575b2781380e85beb
|
data/exe/hind
CHANGED
data/lib/hind/cli.rb
CHANGED
@@ -8,9 +8,8 @@ require 'fileutils'
|
|
8
8
|
module Hind
|
9
9
|
class CLI < Thor
|
10
10
|
class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
|
11
|
-
class_option :config, type: :string, aliases: '-c', desc: 'Path to configuration file'
|
12
11
|
|
13
|
-
desc 'lsif', 'Generate LSIF index'
|
12
|
+
desc 'lsif', 'Generate LSIF index for Ruby classes, modules, and constants'
|
14
13
|
method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
|
15
14
|
method_option :output, type: :string, aliases: '-o', default: 'dump.lsif', desc: 'Output file path'
|
16
15
|
method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
|
@@ -29,30 +28,7 @@ module Hind
|
|
29
28
|
generate_lsif(files, options)
|
30
29
|
say "\nLSIF data has been written to: #{options[:output]}", :green if options[:verbose]
|
31
30
|
rescue => e
|
32
|
-
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
desc 'scip', 'Generate SCIP index'
|
37
|
-
method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
|
38
|
-
method_option :output, type: :string, aliases: '-o', default: 'index.scip', desc: 'Output file path'
|
39
|
-
method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
|
40
|
-
method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
|
41
|
-
method_option :exclude, type: :array, aliases: '-e', desc: 'Patterns to exclude'
|
42
|
-
def scip
|
43
|
-
validate_directory(options[:directory])
|
44
|
-
validate_output_file(options[:output], options[:force])
|
45
|
-
|
46
|
-
files = find_files(options[:directory], options[:glob], options[:exclude])
|
47
|
-
abort "No files found matching pattern '#{options[:glob]}'" if files.empty?
|
48
|
-
|
49
|
-
say "Found #{files.length} files to process", :green if options[:verbose]
|
50
|
-
|
51
|
-
begin
|
52
|
-
generate_scip(files, options)
|
53
|
-
say "\nSCIP data has been written to: #{options[:output]}", :green if options[:verbose]
|
54
|
-
rescue => e
|
55
|
-
abort "Error generating SCIP: #{e.message}"
|
31
|
+
handle_error(e, options[:verbose])
|
56
32
|
end
|
57
33
|
end
|
58
34
|
|
@@ -63,12 +39,65 @@ module Hind
|
|
63
39
|
|
64
40
|
private
|
65
41
|
|
42
|
+
def generate_lsif(files, options)
|
43
|
+
# Initialize generator with absolute project root
|
44
|
+
generator = Hind::LSIF::Generator.new(
|
45
|
+
{
|
46
|
+
vertex_id: 1,
|
47
|
+
initial: true,
|
48
|
+
projectRoot: File.expand_path(options[:directory])
|
49
|
+
}
|
50
|
+
)
|
51
|
+
|
52
|
+
# Create file content map with relative paths
|
53
|
+
file_contents = {}
|
54
|
+
files.each do |file|
|
55
|
+
absolute_path = File.expand_path(file)
|
56
|
+
relative_path = Pathname.new(absolute_path)
|
57
|
+
.relative_path_from(Pathname.new(generator.metadata[:projectRoot]))
|
58
|
+
.to_s
|
59
|
+
|
60
|
+
begin
|
61
|
+
file_contents[relative_path] = File.read(absolute_path)
|
62
|
+
rescue => e
|
63
|
+
warn "Warning: Failed to read file '#{file}': #{e.message}"
|
64
|
+
next
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
File.open(options[:output], 'w') do |output_file|
|
69
|
+
say 'First pass: Collecting declarations...', :cyan if options[:verbose]
|
70
|
+
|
71
|
+
# First pass: Process all files to collect declarations
|
72
|
+
declaration_data = generator.collect_declarations(file_contents)
|
73
|
+
|
74
|
+
say "Found #{declaration_data[:declarations].size} declarations (classes, modules, constants)", :cyan if options[:verbose]
|
75
|
+
say 'Processing files for references...', :cyan if options[:verbose]
|
76
|
+
|
77
|
+
# Second pass: Process each file for references
|
78
|
+
file_contents.each do |relative_path, content|
|
79
|
+
if options[:verbose]
|
80
|
+
say "Processing file: #{relative_path}", :cyan
|
81
|
+
end
|
82
|
+
|
83
|
+
lsif_data = generator.process_file(
|
84
|
+
content: content,
|
85
|
+
uri: relative_path
|
86
|
+
)
|
87
|
+
|
88
|
+
output_file.puts(lsif_data.map(&:to_json).join("\n"))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
66
93
|
def validate_directory(directory)
|
67
94
|
abort "Error: Directory '#{directory}' does not exist" unless Dir.exist?(directory)
|
68
95
|
end
|
69
96
|
|
70
97
|
def validate_output_file(output, force)
|
71
|
-
|
98
|
+
if File.exist?(output) && !force
|
99
|
+
abort "Error: Output file '#{output}' already exists. Use --force to overwrite."
|
100
|
+
end
|
72
101
|
|
73
102
|
# Ensure output directory exists
|
74
103
|
FileUtils.mkdir_p(File.dirname(output))
|
@@ -85,53 +114,10 @@ module Hind
|
|
85
114
|
files
|
86
115
|
end
|
87
116
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
File.open(options[:output], 'w') do |output_file|
|
94
|
-
files.each do |file|
|
95
|
-
say "Processing file: #{file}", :cyan if options[:verbose]
|
96
|
-
|
97
|
-
relative_path = Pathname.new(file).relative_path_from(Pathname.new(options[:directory])).to_s
|
98
|
-
|
99
|
-
begin
|
100
|
-
generator = Hind::LSIF::Generator.new(
|
101
|
-
{
|
102
|
-
uri: relative_path,
|
103
|
-
vertex_id: vertex_id,
|
104
|
-
initial: initial,
|
105
|
-
projectRoot: options[:directory]
|
106
|
-
},
|
107
|
-
global_state
|
108
|
-
)
|
109
|
-
|
110
|
-
output = generator.generate(File.read(file))
|
111
|
-
vertex_id = output.last[:id].to_i + 1
|
112
|
-
output_file.puts(output.map(&:to_json).join("\n"))
|
113
|
-
initial = false
|
114
|
-
rescue => e
|
115
|
-
warn "Warning: Failed to process file '#{file}': #{e.message}"
|
116
|
-
next
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def generate_scip(files, options)
|
123
|
-
raise NotImplementedError, 'SCIP generation not yet implemented'
|
124
|
-
# Similar to generate_lsif but using SCIP generator
|
125
|
-
end
|
126
|
-
|
127
|
-
def load_config(config_path)
|
128
|
-
return {} unless config_path && File.exist?(config_path)
|
129
|
-
|
130
|
-
begin
|
131
|
-
YAML.load_file(config_path) || {}
|
132
|
-
rescue => e
|
133
|
-
abort "Error loading config file: #{e.message}"
|
134
|
-
end
|
117
|
+
def handle_error(error, verbose)
|
118
|
+
message = "Error: #{error.message}"
|
119
|
+
message += "\n#{error.backtrace.join("\n")}" if verbose
|
120
|
+
abort message
|
135
121
|
end
|
136
122
|
end
|
137
123
|
end
|
data/lib/hind/lsif/generator.rb
CHANGED
@@ -3,79 +3,127 @@
|
|
3
3
|
require 'prism'
|
4
4
|
require 'json'
|
5
5
|
require 'uri'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
require_relative 'visitors/declaration_visitor'
|
9
|
+
require_relative 'visitors/reference_visitor'
|
6
10
|
|
7
11
|
module Hind
|
8
12
|
module LSIF
|
9
13
|
class Generator
|
10
14
|
LSIF_VERSION = '0.4.3'
|
11
15
|
|
12
|
-
attr_reader :global_state, :document_id, :
|
16
|
+
attr_reader :metadata, :global_state, :document_id, :current_uri
|
13
17
|
|
14
|
-
def initialize(metadata = {}
|
18
|
+
def initialize(metadata = {})
|
15
19
|
@vertex_id = metadata[:vertex_id] || 1
|
16
20
|
@metadata = {
|
17
21
|
language: 'ruby',
|
18
|
-
projectRoot: Dir.pwd
|
22
|
+
projectRoot: File.expand_path(metadata[:projectRoot] || Dir.pwd)
|
19
23
|
}.merge(metadata)
|
20
24
|
|
21
|
-
@global_state =
|
25
|
+
@global_state = GlobalState.new
|
22
26
|
@document_ids = {}
|
27
|
+
@current_document_id = nil
|
23
28
|
@lsif_data = []
|
29
|
+
@current_uri = nil
|
24
30
|
|
25
31
|
initialize_project if metadata[:initial]
|
26
32
|
end
|
27
33
|
|
28
|
-
def
|
29
|
-
|
34
|
+
def collect_declarations(files)
|
35
|
+
files.each do |path, content|
|
36
|
+
@current_uri = path
|
37
|
+
@document_id = nil
|
38
|
+
@current_document_id = nil
|
39
|
+
|
40
|
+
begin
|
41
|
+
ast = Parser.new(content).parse
|
42
|
+
setup_document
|
43
|
+
visitor = DeclarationVisitor.new(self, path)
|
44
|
+
visitor.visit(ast)
|
45
|
+
finalize_document_state
|
46
|
+
rescue => e
|
47
|
+
warn "Warning: Failed to collect declarations from '#{path}': #{e.message}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
{declarations: @global_state.declarations}
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_file(params)
|
55
|
+
@current_uri = params[:uri]
|
56
|
+
content = params[:content]
|
57
|
+
|
58
|
+
@document_id = nil
|
59
|
+
@current_document_id = nil
|
60
|
+
|
30
61
|
setup_document
|
62
|
+
ast = Parser.new(content).parse
|
31
63
|
|
32
|
-
|
33
|
-
visitor = Visitor.new(self)
|
64
|
+
visitor = ReferenceVisitor.new(self, @current_uri)
|
34
65
|
visitor.visit(ast)
|
35
66
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@lsif_data
|
67
|
+
result = @lsif_data
|
68
|
+
finalize_document_state
|
69
|
+
result
|
40
70
|
end
|
41
71
|
|
42
|
-
def
|
43
|
-
|
44
|
-
start: {
|
45
|
-
line: start_location.start_line - 1,
|
46
|
-
character: start_location.start_column
|
47
|
-
},
|
48
|
-
end: {
|
49
|
-
line: end_location.end_line - 1,
|
50
|
-
character: end_location.end_column
|
51
|
-
}
|
52
|
-
})
|
72
|
+
def register_declaration(declaration)
|
73
|
+
return unless @current_uri && declaration[:node]
|
53
74
|
|
54
|
-
|
55
|
-
@global_state.add_range(file_path, range_id)
|
56
|
-
range_id
|
57
|
-
end
|
75
|
+
qualified_name = declaration[:name]
|
58
76
|
|
59
|
-
|
60
|
-
|
61
|
-
@lsif_data << vertex.to_json
|
62
|
-
@vertex_id += 1
|
63
|
-
@vertex_id - 1
|
64
|
-
end
|
77
|
+
setup_document if @document_id.nil?
|
78
|
+
current_doc_id = @document_id
|
65
79
|
|
66
|
-
|
67
|
-
return unless
|
80
|
+
range_id = create_range(declaration[:node].location, declaration[:node].location)
|
81
|
+
return unless range_id
|
68
82
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
83
|
+
result_set_id = emit_vertex('resultSet')
|
84
|
+
emit_edge('next', range_id, result_set_id)
|
85
|
+
|
86
|
+
def_result_id = emit_vertex('definitionResult')
|
87
|
+
emit_edge('textDocument/definition', result_set_id, def_result_id)
|
88
|
+
|
89
|
+
emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
|
90
|
+
|
91
|
+
hover_content = generate_hover_content(declaration)
|
92
|
+
hover_id = emit_vertex('hoverResult', {
|
93
|
+
contents: [{
|
94
|
+
language: 'ruby',
|
95
|
+
value: hover_content
|
96
|
+
}]
|
97
|
+
})
|
98
|
+
emit_edge('textDocument/hover', result_set_id, hover_id)
|
99
|
+
|
100
|
+
@global_state.add_declaration(qualified_name, {
|
101
|
+
type: declaration[:type],
|
102
|
+
scope: declaration[:scope],
|
103
|
+
file: @current_uri,
|
104
|
+
range_id: range_id,
|
105
|
+
result_set_id: result_set_id,
|
106
|
+
document_id: current_doc_id
|
107
|
+
}.merge(declaration))
|
108
|
+
|
109
|
+
result_set_id
|
73
110
|
end
|
74
111
|
|
75
|
-
def
|
76
|
-
|
77
|
-
@global_state.
|
78
|
-
|
112
|
+
def register_reference(reference)
|
113
|
+
return unless @current_uri && reference[:node]
|
114
|
+
return unless @global_state.has_declaration?(reference[:name])
|
115
|
+
|
116
|
+
setup_document if @document_id.nil?
|
117
|
+
current_doc_id = @document_id
|
118
|
+
|
119
|
+
range_id = create_range(reference[:node].location, reference[:node].location)
|
120
|
+
return unless range_id
|
121
|
+
|
122
|
+
declaration = @global_state.declarations[reference[:name]]
|
123
|
+
return unless declaration[:result_set_id]
|
124
|
+
|
125
|
+
@global_state.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
|
126
|
+
emit_edge('next', range_id, declaration[:result_set_id])
|
79
127
|
end
|
80
128
|
|
81
129
|
private
|
@@ -87,7 +135,7 @@ module Hind
|
|
87
135
|
positionEncoding: 'utf-16',
|
88
136
|
toolInfo: {
|
89
137
|
name: 'hind',
|
90
|
-
version: VERSION
|
138
|
+
version: Hind::VERSION
|
91
139
|
}
|
92
140
|
})
|
93
141
|
|
@@ -95,89 +143,138 @@ module Hind
|
|
95
143
|
end
|
96
144
|
|
97
145
|
def setup_document
|
98
|
-
|
146
|
+
return if @document_id
|
147
|
+
return unless @current_uri
|
148
|
+
|
149
|
+
file_path = File.join(@metadata[:projectRoot], @current_uri)
|
99
150
|
|
100
151
|
@document_id = emit_vertex('document', {
|
101
152
|
uri: path_to_uri(file_path),
|
102
153
|
languageId: 'ruby'
|
103
154
|
})
|
104
|
-
|
155
|
+
|
156
|
+
@document_ids[@current_uri] = @document_id
|
157
|
+
@current_document_id = @document_id
|
105
158
|
|
106
159
|
emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
|
107
160
|
end
|
108
161
|
|
109
|
-
def
|
110
|
-
|
111
|
-
|
162
|
+
def finalize_document_state
|
163
|
+
return unless @current_uri && @document_id
|
164
|
+
|
165
|
+
ranges = @global_state.get_ranges_for_file(@current_uri)
|
166
|
+
if ranges&.any?
|
167
|
+
emit_edge('contains', @document_id, ranges, nil, @document_id)
|
168
|
+
end
|
169
|
+
end
|
112
170
|
|
113
|
-
|
171
|
+
def create_range(start_location, end_location)
|
172
|
+
return nil unless @current_uri && start_location && end_location
|
114
173
|
|
115
|
-
|
174
|
+
range_id = emit_vertex('range', {
|
175
|
+
start: {
|
176
|
+
line: start_location.start_line - 1,
|
177
|
+
character: start_location.start_column
|
178
|
+
},
|
179
|
+
end: {
|
180
|
+
line: end_location.end_line - 1,
|
181
|
+
character: end_location.end_column
|
182
|
+
}
|
183
|
+
})
|
184
|
+
|
185
|
+
@global_state.add_range(@current_uri, range_id)
|
186
|
+
range_id
|
116
187
|
end
|
117
188
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
189
|
+
def emit_vertex(label, data = nil)
|
190
|
+
vertex = {
|
191
|
+
id: @vertex_id,
|
192
|
+
type: 'vertex',
|
193
|
+
label: label
|
194
|
+
}
|
195
|
+
|
196
|
+
if data
|
197
|
+
if %w[hoverResult definitionResult referenceResult].include?(label)
|
198
|
+
vertex[:result] = format_hover_data(data)
|
199
|
+
else
|
200
|
+
vertex.merge!(data)
|
201
|
+
end
|
202
|
+
end
|
122
203
|
|
123
|
-
|
124
|
-
|
204
|
+
@lsif_data << vertex
|
205
|
+
@vertex_id += 1
|
206
|
+
@vertex_id - 1
|
207
|
+
end
|
125
208
|
|
126
|
-
|
127
|
-
|
209
|
+
def emit_edge(label, out_v, in_v, property = nil, doc_id = nil)
|
210
|
+
return unless out_v && valid_in_v?(in_v)
|
128
211
|
|
129
|
-
|
130
|
-
|
131
|
-
|
212
|
+
edge = {
|
213
|
+
id: @vertex_id,
|
214
|
+
type: 'edge',
|
215
|
+
label: label,
|
216
|
+
outV: out_v
|
217
|
+
}
|
132
218
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
219
|
+
if in_v.is_a?(Array)
|
220
|
+
edge[:inVs] = in_v
|
221
|
+
else
|
222
|
+
edge[:inV] = in_v
|
223
|
+
end
|
138
224
|
|
139
|
-
|
140
|
-
|
225
|
+
if label == 'item'
|
226
|
+
edge[:document] = doc_id || @current_document_id
|
227
|
+
edge[:property] = property if property
|
228
|
+
end
|
141
229
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
230
|
+
@lsif_data << edge
|
231
|
+
@vertex_id += 1
|
232
|
+
@vertex_id - 1
|
233
|
+
end
|
234
|
+
|
235
|
+
def generate_hover_content(declaration)
|
236
|
+
case declaration[:type]
|
237
|
+
when :class
|
238
|
+
hover = ["class #{declaration[:name]}"]
|
239
|
+
hover << " < #{declaration[:superclass]}" if declaration[:superclass]
|
240
|
+
hover.join
|
241
|
+
when :module
|
242
|
+
"module #{declaration[:name]}"
|
243
|
+
when :constant
|
244
|
+
value_info = declaration[:node].value ? " = #{declaration[:node].value.inspect}" : ''
|
245
|
+
"#{declaration[:name]}#{value_info}"
|
246
|
+
else
|
247
|
+
declaration[:name].to_s
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def format_hover_data(data)
|
252
|
+
return data unless data[:contents]
|
253
|
+
|
254
|
+
data[:contents] = data[:contents].map do |content|
|
255
|
+
content[:value] = strip_code_block(content[:value])
|
256
|
+
content
|
147
257
|
end
|
258
|
+
data
|
259
|
+
end
|
260
|
+
|
261
|
+
def strip_code_block(text)
|
262
|
+
text.gsub(/```.*\n?/, '').strip
|
148
263
|
end
|
149
264
|
|
150
265
|
def valid_in_v?(in_v)
|
151
266
|
return false unless in_v
|
152
267
|
return in_v.any? if in_v.is_a?(Array)
|
153
|
-
|
154
268
|
true
|
155
269
|
end
|
156
270
|
|
157
|
-
def edge_document(label)
|
158
|
-
(label == 'item') ? @document_id : nil
|
159
|
-
end
|
160
|
-
|
161
271
|
def path_to_uri(path)
|
272
|
+
return nil unless path
|
162
273
|
normalized_path = path.tr('\\', '/')
|
163
274
|
normalized_path = normalized_path.sub(%r{^file://}, '')
|
164
275
|
absolute_path = File.expand_path(normalized_path)
|
165
276
|
"file://#{absolute_path}"
|
166
277
|
end
|
167
|
-
|
168
|
-
def make_hover_content(text)
|
169
|
-
{
|
170
|
-
contents: [{
|
171
|
-
language: 'ruby',
|
172
|
-
value: strip_code_block(text)
|
173
|
-
}]
|
174
|
-
}
|
175
|
-
end
|
176
|
-
|
177
|
-
def strip_code_block(text)
|
178
|
-
# Strip any existing code block markers and normalize
|
179
|
-
text.gsub(/```.*\n?/, '').strip
|
180
|
-
end
|
181
278
|
end
|
182
279
|
end
|
183
280
|
end
|
@@ -1,17 +1,32 @@
|
|
1
|
+
# lib/hind/lsif/global_state.rb
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module Hind
|
4
5
|
module LSIF
|
5
6
|
class GlobalState
|
6
|
-
attr_reader :result_sets, :definitions, :references, :ranges
|
7
7
|
attr_accessor :project_id
|
8
|
+
attr_reader :declarations, :references, :result_sets, :ranges
|
8
9
|
|
9
10
|
def initialize
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@ranges = {}
|
14
|
-
@project_id = nil
|
11
|
+
@declarations = {} # {qualified_name => {type:, node:, scope:, file:, range_id:, result_set_id:}}
|
12
|
+
@references = {} # {qualified_name => [{file:, range_id:, type:}, ...]}
|
13
|
+
@result_sets = {} # {qualified_name => result_set_id}
|
14
|
+
@ranges = {} # {file_path => [range_ids]}
|
15
|
+
@project_id = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_declaration(qualified_name, data)
|
19
|
+
@declarations[qualified_name] = data
|
20
|
+
@result_sets[qualified_name] = data[:result_set_id] if data[:result_set_id]
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_reference(qualified_name, file_path, range_id, document_id)
|
24
|
+
@references[qualified_name] ||= []
|
25
|
+
@references[qualified_name] << {
|
26
|
+
file: file_path,
|
27
|
+
range_id: range_id,
|
28
|
+
document_id: document_id
|
29
|
+
}
|
15
30
|
end
|
16
31
|
|
17
32
|
def add_range(file_path, range_id)
|
@@ -19,13 +34,60 @@ module Hind
|
|
19
34
|
@ranges[file_path] << range_id
|
20
35
|
end
|
21
36
|
|
22
|
-
def
|
23
|
-
@
|
37
|
+
def has_declaration?(qualified_name)
|
38
|
+
@declarations.key?(qualified_name)
|
24
39
|
end
|
25
40
|
|
26
|
-
def
|
27
|
-
@
|
28
|
-
|
41
|
+
def get_declaration(qualified_name)
|
42
|
+
@declarations[qualified_name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_references(qualified_name)
|
46
|
+
@references[qualified_name] || []
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_result_set(qualified_name)
|
50
|
+
@result_sets[qualified_name]
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_ranges_for_file(file_path)
|
54
|
+
@ranges[file_path] || []
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_constant_declaration(name, current_scope)
|
58
|
+
return name if has_declaration?(name)
|
59
|
+
|
60
|
+
if current_scope && !current_scope.empty?
|
61
|
+
qualified_name = "#{current_scope}::#{name}"
|
62
|
+
return qualified_name if has_declaration?(qualified_name)
|
63
|
+
|
64
|
+
scope_parts = current_scope.split('::')
|
65
|
+
while scope_parts.any?
|
66
|
+
scope_parts.pop
|
67
|
+
qualified_name = scope_parts.empty? ? name : "#{scope_parts.join("::")}::#{name}"
|
68
|
+
return qualified_name if has_declaration?(qualified_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
has_declaration?(name) ? name : nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def debug_info
|
76
|
+
{
|
77
|
+
declarations_count: @declarations.size,
|
78
|
+
references_count: @references.values.sum(&:size),
|
79
|
+
result_sets_count: @result_sets.size,
|
80
|
+
ranges_count: @ranges.values.sum(&:size),
|
81
|
+
declaration_types: declaration_types_count
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def declaration_types_count
|
88
|
+
@declarations.values.each_with_object(Hash.new(0)) do |decl, counts|
|
89
|
+
counts[decl[:type]] += 1
|
90
|
+
end
|
29
91
|
end
|
30
92
|
end
|
31
93
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# lib/hind/lsif/visitors/declaration_visitor.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Hind
|
5
|
+
module LSIF
|
6
|
+
class DeclarationVisitor < Prism::Visitor
|
7
|
+
attr_reader :current_scope
|
8
|
+
|
9
|
+
def initialize(generator, file_path)
|
10
|
+
@generator = generator
|
11
|
+
@file_path = file_path
|
12
|
+
@current_scope = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_class_node(node)
|
16
|
+
@current_scope.push(node.constant_path.slice)
|
17
|
+
class_name = current_scope_name
|
18
|
+
|
19
|
+
@generator.register_declaration({
|
20
|
+
type: :class,
|
21
|
+
name: class_name,
|
22
|
+
node: node,
|
23
|
+
scope: @current_scope[0..-2].join('::'),
|
24
|
+
superclass: node.superclass&.slice
|
25
|
+
})
|
26
|
+
|
27
|
+
super
|
28
|
+
@current_scope.pop
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_module_node(node)
|
32
|
+
@current_scope.push(node.constant_path.slice)
|
33
|
+
module_name = current_scope_name
|
34
|
+
|
35
|
+
@generator.register_declaration({
|
36
|
+
type: :module,
|
37
|
+
name: module_name,
|
38
|
+
node: node,
|
39
|
+
scope: @current_scope[0..-2].join('::')
|
40
|
+
})
|
41
|
+
|
42
|
+
super
|
43
|
+
@current_scope.pop
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_constant_write_node(node)
|
47
|
+
return unless node.name
|
48
|
+
|
49
|
+
constant_name = node.name.to_s
|
50
|
+
qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
|
51
|
+
|
52
|
+
@generator.register_declaration({
|
53
|
+
type: :constant,
|
54
|
+
name: qualified_name,
|
55
|
+
node: node,
|
56
|
+
scope: current_scope_name
|
57
|
+
})
|
58
|
+
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def current_scope_name
|
65
|
+
@current_scope.join('::')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hind
|
4
|
+
module LSIF
|
5
|
+
class ReferenceVisitor < Prism::Visitor
|
6
|
+
attr_reader :current_scope
|
7
|
+
|
8
|
+
def initialize(generator, file_path)
|
9
|
+
@generator = generator
|
10
|
+
@file_path = file_path
|
11
|
+
@current_scope = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def visit_constant_read_node(node)
|
15
|
+
return unless node.name
|
16
|
+
|
17
|
+
constant_name = node.name.to_s
|
18
|
+
qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
|
19
|
+
|
20
|
+
@generator.register_reference({
|
21
|
+
type: :constant,
|
22
|
+
name: qualified_name,
|
23
|
+
node: node,
|
24
|
+
scope: current_scope_name
|
25
|
+
})
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_constant_path_node(node)
|
31
|
+
qualified_name = node.slice
|
32
|
+
|
33
|
+
@generator.register_reference({
|
34
|
+
type: :constant,
|
35
|
+
name: qualified_name,
|
36
|
+
node: node,
|
37
|
+
scope: current_scope_name
|
38
|
+
})
|
39
|
+
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def visit_class_node(node)
|
44
|
+
@current_scope.push(node.constant_path.slice)
|
45
|
+
super
|
46
|
+
@current_scope.pop
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_module_node(node)
|
50
|
+
@current_scope.push(node.constant_path.slice)
|
51
|
+
super
|
52
|
+
@current_scope.pop
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def current_scope_name
|
58
|
+
@current_scope.join('::')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/hind/lsif.rb
CHANGED
data/lib/hind/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hind
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aboobacker MK
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-02-
|
10
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: prism
|
@@ -111,7 +111,8 @@ files:
|
|
111
111
|
- lib/hind/lsif/generator.rb
|
112
112
|
- lib/hind/lsif/global_state.rb
|
113
113
|
- lib/hind/lsif/vertex.rb
|
114
|
-
- lib/hind/lsif/
|
114
|
+
- lib/hind/lsif/visitors/declaration_visitor.rb
|
115
|
+
- lib/hind/lsif/visitors/reference_visitor.rb
|
115
116
|
- lib/hind/parser.rb
|
116
117
|
- lib/hind/scip.rb
|
117
118
|
- lib/hind/scip/generator.rb
|
data/lib/hind/lsif/visitor.rb
DELETED
@@ -1,268 +0,0 @@
|
|
1
|
-
# lib/hind/lsif/visitor.rb
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module Hind
|
5
|
-
module LSIF
|
6
|
-
class Visitor
|
7
|
-
def initialize(generator)
|
8
|
-
@generator = generator
|
9
|
-
@current_scope = []
|
10
|
-
end
|
11
|
-
|
12
|
-
def visit(node)
|
13
|
-
return unless node
|
14
|
-
|
15
|
-
method_name = "visit_#{node.class.name.split("::").last.downcase}"
|
16
|
-
if respond_to?(method_name)
|
17
|
-
send(method_name, node)
|
18
|
-
else
|
19
|
-
visit_children(node)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def visit_children(node)
|
24
|
-
node.child_nodes.each { |child| visit(child) if child }
|
25
|
-
end
|
26
|
-
|
27
|
-
def visit_defnode(node)
|
28
|
-
# Handle method definitions
|
29
|
-
method_name = node.name.to_s
|
30
|
-
qualified_name = current_scope_name.empty? ? method_name : "#{current_scope_name}##{method_name}"
|
31
|
-
|
32
|
-
range_id = @generator.create_range(node.location, node.location)
|
33
|
-
result_set_id = @generator.emit_vertex('resultSet')
|
34
|
-
@generator.emit_edge('next', range_id, result_set_id)
|
35
|
-
|
36
|
-
def_result_id = @generator.emit_vertex('definitionResult')
|
37
|
-
@generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
|
38
|
-
@generator.emit_edge('item', def_result_id, [range_id], 'definitions')
|
39
|
-
|
40
|
-
# Generate method signature for hover
|
41
|
-
sig = []
|
42
|
-
sig << "def #{qualified_name}"
|
43
|
-
sig << "(#{node.parameters.slice})" if node.parameters
|
44
|
-
|
45
|
-
hover_id = @generator.emit_vertex('hoverResult', {
|
46
|
-
contents: [{
|
47
|
-
language: 'ruby',
|
48
|
-
value: sig.join
|
49
|
-
}]
|
50
|
-
})
|
51
|
-
@generator.emit_edge('textDocument/hover', result_set_id, hover_id)
|
52
|
-
|
53
|
-
@generator.add_to_global_state(qualified_name, result_set_id, range_id)
|
54
|
-
|
55
|
-
visit_children(node)
|
56
|
-
end
|
57
|
-
|
58
|
-
def visit_classnode(node)
|
59
|
-
@current_scope.push(node.constant_path.slice)
|
60
|
-
class_name = current_scope_name
|
61
|
-
|
62
|
-
range_id = @generator.create_range(node.location, node.location)
|
63
|
-
result_set_id = @generator.emit_vertex('resultSet')
|
64
|
-
@generator.emit_edge('next', range_id, result_set_id)
|
65
|
-
|
66
|
-
def_result_id = @generator.emit_vertex('definitionResult')
|
67
|
-
@generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
|
68
|
-
@generator.emit_edge('item', def_result_id, [range_id], 'definitions')
|
69
|
-
|
70
|
-
# Generate hover with inheritance info
|
71
|
-
hover = []
|
72
|
-
class_def = "class #{class_name}"
|
73
|
-
class_def += " < #{node.superclass.slice}" if node.superclass
|
74
|
-
|
75
|
-
hover << if node.superclass
|
76
|
-
"#{class_def}\n\nInherits from: #{node.superclass.slice}"
|
77
|
-
else
|
78
|
-
class_def
|
79
|
-
end
|
80
|
-
|
81
|
-
hover_id = @generator.emit_vertex('hoverResult', {
|
82
|
-
contents: [{
|
83
|
-
language: 'ruby',
|
84
|
-
value: hover.join("\n")
|
85
|
-
}]
|
86
|
-
})
|
87
|
-
@generator.emit_edge('textDocument/hover', result_set_id, hover_id)
|
88
|
-
|
89
|
-
@generator.add_to_global_state(class_name, result_set_id, range_id)
|
90
|
-
|
91
|
-
# Handle inheritance
|
92
|
-
visit_inheritance(node.superclass) if node.superclass
|
93
|
-
|
94
|
-
visit_children(node)
|
95
|
-
@current_scope.pop
|
96
|
-
end
|
97
|
-
|
98
|
-
def visit_modulenode(node)
|
99
|
-
@current_scope.push(node.constant_path.slice)
|
100
|
-
module_name = current_scope_name
|
101
|
-
|
102
|
-
range_id = @generator.create_range(node.location, node.location)
|
103
|
-
result_set_id = @generator.emit_vertex('resultSet')
|
104
|
-
@generator.emit_edge('next', range_id, result_set_id)
|
105
|
-
|
106
|
-
def_result_id = @generator.emit_vertex('definitionResult')
|
107
|
-
@generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
|
108
|
-
@generator.emit_edge('item', def_result_id, [range_id], 'definitions')
|
109
|
-
|
110
|
-
hover_id = @generator.emit_vertex('hoverResult', {
|
111
|
-
contents: [{
|
112
|
-
language: 'ruby',
|
113
|
-
value: "module #{module_name}"
|
114
|
-
}]
|
115
|
-
})
|
116
|
-
@generator.emit_edge('textDocument/hover', result_set_id, hover_id)
|
117
|
-
|
118
|
-
@generator.add_to_global_state(module_name, result_set_id, range_id)
|
119
|
-
|
120
|
-
visit_children(node)
|
121
|
-
@current_scope.pop
|
122
|
-
end
|
123
|
-
|
124
|
-
def visit_callnode(node)
|
125
|
-
return unless node.name && node.location
|
126
|
-
|
127
|
-
method_name = node.name.to_s
|
128
|
-
qualified_names = []
|
129
|
-
|
130
|
-
# Try with current scope first
|
131
|
-
qualified_names << "#{current_scope_name}##{method_name}" unless current_scope_name.empty?
|
132
|
-
|
133
|
-
# Try with receiver's type if available
|
134
|
-
if node.receiver
|
135
|
-
case node.receiver
|
136
|
-
when Prism::ConstantReadNode
|
137
|
-
qualified_names << "#{node.receiver.name}##{method_name}"
|
138
|
-
when Prism::CallNode
|
139
|
-
# Handle method chaining
|
140
|
-
qualified_names << "#{node.receiver.name}##{method_name}" if node.receiver.name
|
141
|
-
when Prism::InstanceVariableReadNode
|
142
|
-
# Handle instance variable calls
|
143
|
-
qualified_names << "#{current_scope_name}##{method_name}" if current_scope_name
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
# Try as a standalone method
|
148
|
-
qualified_names << method_name
|
149
|
-
|
150
|
-
# Add references for matching qualified names
|
151
|
-
qualified_names.each do |qualified_name|
|
152
|
-
next unless @generator.global_state.result_sets[qualified_name]
|
153
|
-
|
154
|
-
range_id = @generator.create_range(node.location, node.location)
|
155
|
-
@generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
|
156
|
-
@generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
|
157
|
-
break # Stop after finding first match
|
158
|
-
end
|
159
|
-
|
160
|
-
visit_children(node)
|
161
|
-
end
|
162
|
-
|
163
|
-
def visit_constantreadnode(node)
|
164
|
-
return unless node.name
|
165
|
-
|
166
|
-
constant_name = node.name.to_s
|
167
|
-
qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
|
168
|
-
|
169
|
-
return unless @generator.global_state.result_sets[qualified_name]
|
170
|
-
|
171
|
-
range_id = @generator.create_range(node.location, node.location)
|
172
|
-
@generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
|
173
|
-
@generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
|
174
|
-
end
|
175
|
-
|
176
|
-
def visit_constantwritenode(node)
|
177
|
-
return unless node.name
|
178
|
-
|
179
|
-
constant_name = node.name.to_s
|
180
|
-
qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
|
181
|
-
|
182
|
-
range_id = @generator.create_range(node.location, node.location)
|
183
|
-
result_set_id = @generator.emit_vertex('resultSet')
|
184
|
-
@generator.emit_edge('next', range_id, result_set_id)
|
185
|
-
|
186
|
-
def_result_id = @generator.emit_vertex('definitionResult')
|
187
|
-
@generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
|
188
|
-
@generator.emit_edge('item', def_result_id, [range_id], 'definitions')
|
189
|
-
|
190
|
-
hover_id = @generator.emit_vertex('hoverResult', {
|
191
|
-
contents: [{
|
192
|
-
language: 'ruby',
|
193
|
-
value: "#{qualified_name} = ..."
|
194
|
-
}]
|
195
|
-
})
|
196
|
-
@generator.emit_edge('textDocument/hover', result_set_id, hover_id)
|
197
|
-
|
198
|
-
@generator.add_to_global_state(qualified_name, result_set_id, range_id)
|
199
|
-
|
200
|
-
visit_children(node)
|
201
|
-
end
|
202
|
-
|
203
|
-
def visit_instancevariablereadnode(node)
|
204
|
-
return unless node.name && current_scope_name
|
205
|
-
|
206
|
-
var_name = node.name.to_s
|
207
|
-
qualified_name = "#{current_scope_name}##{var_name}"
|
208
|
-
|
209
|
-
return unless @generator.global_state.result_sets[qualified_name]
|
210
|
-
|
211
|
-
range_id = @generator.create_range(node.location, node.location)
|
212
|
-
@generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
|
213
|
-
@generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
|
214
|
-
end
|
215
|
-
|
216
|
-
def visit_instancevariablewritenode(node)
|
217
|
-
return unless node.name && current_scope_name
|
218
|
-
|
219
|
-
var_name = node.name.to_s
|
220
|
-
qualified_name = "#{current_scope_name}##{var_name}"
|
221
|
-
|
222
|
-
range_id = @generator.create_range(node.location, node.location)
|
223
|
-
result_set_id = @generator.emit_vertex('resultSet')
|
224
|
-
@generator.emit_edge('next', range_id, result_set_id)
|
225
|
-
|
226
|
-
def_result_id = @generator.emit_vertex('definitionResult')
|
227
|
-
@generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
|
228
|
-
@generator.emit_edge('item', def_result_id, [range_id], 'definitions')
|
229
|
-
|
230
|
-
hover_id = @generator.emit_vertex('hoverResult', {
|
231
|
-
contents: [{
|
232
|
-
language: 'ruby',
|
233
|
-
value: "Instance variable #{var_name} in #{current_scope_name}"
|
234
|
-
}]
|
235
|
-
})
|
236
|
-
@generator.emit_edge('textDocument/hover', result_set_id, hover_id)
|
237
|
-
|
238
|
-
@generator.add_to_global_state(qualified_name, result_set_id, range_id)
|
239
|
-
|
240
|
-
visit_children(node)
|
241
|
-
end
|
242
|
-
|
243
|
-
private
|
244
|
-
|
245
|
-
def current_scope_name
|
246
|
-
@current_scope.join('::')
|
247
|
-
end
|
248
|
-
|
249
|
-
def visit_inheritance(node)
|
250
|
-
case node
|
251
|
-
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
252
|
-
range_id = @generator.create_range(node.location, node.location)
|
253
|
-
qualified_name = case node
|
254
|
-
when Prism::ConstantReadNode
|
255
|
-
node.name.to_s
|
256
|
-
when Prism::ConstantPathNode
|
257
|
-
node.slice
|
258
|
-
end
|
259
|
-
|
260
|
-
return unless @generator.global_state.result_sets[qualified_name]
|
261
|
-
|
262
|
-
@generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
|
263
|
-
@generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|