hind 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|