hind 0.1.5 → 0.1.6
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 +189 -68
- data/lib/hind/lsif/generator.rb +219 -91
- data/lib/hind/lsif/global_state.rb +150 -11
- data/lib/hind/lsif/visitor.rb +28 -41
- data/lib/hind/lsif/visitors/declaration_visitor.rb +239 -0
- data/lib/hind/lsif/visitors/reference_visitor.rb +221 -0
- data/lib/hind/version.rb +1 -1
- metadata +4 -2
data/lib/hind/lsif/generator.rb
CHANGED
@@ -3,79 +3,159 @@
|
|
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 = {}
|
23
27
|
@lsif_data = []
|
28
|
+
@current_uri = nil
|
24
29
|
|
25
30
|
initialize_project if metadata[:initial]
|
26
31
|
end
|
27
32
|
|
28
|
-
def
|
29
|
-
|
33
|
+
def collect_declarations(files)
|
34
|
+
files.each do |path, content|
|
35
|
+
@current_uri = path
|
36
|
+
ast = Parser.new(content).parse
|
37
|
+
visitor = DeclarationVisitor.new(self, path)
|
38
|
+
visitor.visit(ast)
|
39
|
+
end
|
40
|
+
|
41
|
+
{ declarations: @global_state.declarations }
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_file(params)
|
45
|
+
content = params[:content]
|
46
|
+
@current_uri = params[:uri]
|
47
|
+
|
30
48
|
setup_document
|
49
|
+
ast = Parser.new(content).parse
|
31
50
|
|
32
|
-
|
33
|
-
visitor =
|
51
|
+
# Process declarations first to update any missing ones
|
52
|
+
visitor = DeclarationVisitor.new(self, @current_uri)
|
34
53
|
visitor.visit(ast)
|
35
54
|
|
36
|
-
|
37
|
-
|
55
|
+
# Then process references
|
56
|
+
visitor = ReferenceVisitor.new(self, @current_uri)
|
57
|
+
visitor.visit(ast)
|
38
58
|
|
59
|
+
finalize_document
|
39
60
|
@lsif_data
|
40
61
|
end
|
41
62
|
|
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
|
-
})
|
63
|
+
def register_declaration(declaration)
|
64
|
+
return unless @current_uri && declaration[:node]
|
53
65
|
|
54
|
-
|
55
|
-
|
56
|
-
range_id
|
57
|
-
end
|
66
|
+
qualified_name = declaration[:name]
|
67
|
+
range_id = create_range(declaration[:node].location, declaration[:node].location)
|
68
|
+
return unless range_id
|
58
69
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
70
|
+
result_set_id = emit_vertex('resultSet')
|
71
|
+
emit_edge('next', range_id, result_set_id)
|
72
|
+
|
73
|
+
def_result_id = emit_vertex('definitionResult')
|
74
|
+
emit_edge('textDocument/definition', result_set_id, def_result_id)
|
75
|
+
emit_edge('item', def_result_id, [range_id], 'definitions')
|
76
|
+
|
77
|
+
hover_content = generate_hover_content(declaration)
|
78
|
+
hover_id = emit_vertex('hoverResult', {
|
79
|
+
contents: [{
|
80
|
+
language: 'ruby',
|
81
|
+
value: hover_content
|
82
|
+
}]
|
83
|
+
})
|
84
|
+
emit_edge('textDocument/hover', result_set_id, hover_id)
|
85
|
+
|
86
|
+
@global_state.add_declaration(qualified_name, {
|
87
|
+
type: declaration[:type],
|
88
|
+
scope: declaration[:scope],
|
89
|
+
file: @current_uri,
|
90
|
+
range_id: range_id,
|
91
|
+
result_set_id: result_set_id
|
92
|
+
}.merge(declaration))
|
64
93
|
end
|
65
94
|
|
66
|
-
def
|
67
|
-
return unless
|
95
|
+
def register_reference(reference)
|
96
|
+
return unless @current_uri && reference[:node]
|
97
|
+
return unless @global_state.has_declaration?(reference[:name])
|
68
98
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
99
|
+
range_id = create_range(reference[:node].location, reference[:node].location)
|
100
|
+
return unless range_id
|
101
|
+
|
102
|
+
declaration = @global_state.declarations[reference[:name]]
|
103
|
+
@global_state.add_reference(reference[:name], @current_uri, range_id)
|
104
|
+
emit_edge('next', range_id, declaration[:result_set_id])
|
73
105
|
end
|
74
106
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
@global_state.
|
107
|
+
def finalize_cross_references
|
108
|
+
cross_ref_data = []
|
109
|
+
|
110
|
+
@global_state.references.each do |qualified_name, references|
|
111
|
+
declaration = @global_state.declarations[qualified_name]
|
112
|
+
next unless declaration
|
113
|
+
|
114
|
+
result_set_id = declaration[:result_set_id]
|
115
|
+
next unless result_set_id
|
116
|
+
|
117
|
+
ref_result_id = emit_vertex('referenceResult')
|
118
|
+
emit_edge('textDocument/references', result_set_id, ref_result_id)
|
119
|
+
|
120
|
+
# Collect all reference range IDs
|
121
|
+
all_refs = references.map { |ref| ref[:range_id] }
|
122
|
+
all_refs << declaration[:range_id] if declaration[:range_id]
|
123
|
+
|
124
|
+
# Group references by document
|
125
|
+
references.group_by { |ref| ref[:file] }.each do |file_path, file_refs|
|
126
|
+
document_id = @document_ids[file_path]
|
127
|
+
next unless document_id
|
128
|
+
|
129
|
+
cross_ref_data << {
|
130
|
+
id: @vertex_id,
|
131
|
+
type: 'edge',
|
132
|
+
label: 'item',
|
133
|
+
outV: ref_result_id,
|
134
|
+
inVs: all_refs,
|
135
|
+
document: document_id,
|
136
|
+
property: 'references'
|
137
|
+
}
|
138
|
+
@vertex_id += 1
|
139
|
+
end
|
140
|
+
|
141
|
+
# Handle document containing the definition
|
142
|
+
def_file = declaration[:file]
|
143
|
+
def_document_id = @document_ids[def_file]
|
144
|
+
if def_document_id && references.none? { |ref| ref[:file] == def_file }
|
145
|
+
cross_ref_data << {
|
146
|
+
id: @vertex_id,
|
147
|
+
type: 'edge',
|
148
|
+
label: 'item',
|
149
|
+
outV: ref_result_id,
|
150
|
+
inVs: all_refs,
|
151
|
+
document: def_document_id,
|
152
|
+
property: 'references'
|
153
|
+
}
|
154
|
+
@vertex_id += 1
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
cross_ref_data
|
79
159
|
end
|
80
160
|
|
81
161
|
private
|
@@ -87,97 +167,145 @@ module Hind
|
|
87
167
|
positionEncoding: 'utf-16',
|
88
168
|
toolInfo: {
|
89
169
|
name: 'hind',
|
90
|
-
version: VERSION
|
170
|
+
version: Hind::VERSION
|
91
171
|
}
|
92
172
|
})
|
93
173
|
|
94
|
-
@global_state.project_id = emit_vertex('project', {kind: 'ruby'})
|
174
|
+
@global_state.project_id = emit_vertex('project', { kind: 'ruby' })
|
95
175
|
end
|
96
176
|
|
97
177
|
def setup_document
|
98
|
-
|
178
|
+
return unless @current_uri
|
179
|
+
|
180
|
+
file_path = File.join(@metadata[:projectRoot], @current_uri)
|
99
181
|
|
100
182
|
@document_id = emit_vertex('document', {
|
101
183
|
uri: path_to_uri(file_path),
|
102
184
|
languageId: 'ruby'
|
103
185
|
})
|
104
|
-
@document_ids[
|
186
|
+
@document_ids[@current_uri] = @document_id
|
105
187
|
|
106
188
|
emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
|
107
189
|
end
|
108
190
|
|
109
191
|
def finalize_document
|
110
|
-
|
111
|
-
ranges = @global_state.ranges[file_path]
|
192
|
+
return unless @current_uri
|
112
193
|
|
113
|
-
|
194
|
+
ranges = @global_state.get_ranges_for_file(@current_uri)
|
195
|
+
if ranges&.any?
|
196
|
+
emit_edge('contains', @document_id, ranges)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def create_range(start_location, end_location)
|
201
|
+
return nil unless @current_uri && start_location && end_location
|
114
202
|
|
115
|
-
|
203
|
+
range_id = emit_vertex('range', {
|
204
|
+
start: {
|
205
|
+
line: start_location.start_line - 1,
|
206
|
+
character: start_location.start_column
|
207
|
+
},
|
208
|
+
end: {
|
209
|
+
line: end_location.end_line - 1,
|
210
|
+
character: end_location.end_column
|
211
|
+
}
|
212
|
+
})
|
213
|
+
|
214
|
+
@global_state.add_range(@current_uri, range_id)
|
215
|
+
range_id
|
116
216
|
end
|
117
217
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
218
|
+
def emit_vertex(label, data = nil)
|
219
|
+
vertex = {
|
220
|
+
id: @vertex_id,
|
221
|
+
type: 'vertex',
|
222
|
+
label: label
|
223
|
+
}
|
122
224
|
|
123
|
-
|
124
|
-
|
225
|
+
if data
|
226
|
+
if %w[hoverResult definitionResult referenceResult].include?(label)
|
227
|
+
vertex[:result] = format_hover_data(data)
|
228
|
+
else
|
229
|
+
vertex.merge!(data)
|
230
|
+
end
|
231
|
+
end
|
125
232
|
|
126
|
-
|
127
|
-
|
233
|
+
@lsif_data << vertex
|
234
|
+
@vertex_id += 1
|
235
|
+
@vertex_id - 1
|
236
|
+
end
|
128
237
|
|
129
|
-
|
130
|
-
|
131
|
-
all_refs << definition[:range_id]
|
238
|
+
def emit_edge(label, out_v, in_v, property = nil)
|
239
|
+
return unless out_v && valid_in_v?(in_v)
|
132
240
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
241
|
+
edge = {
|
242
|
+
id: @vertex_id,
|
243
|
+
type: 'edge',
|
244
|
+
label: label,
|
245
|
+
outV: out_v
|
246
|
+
}
|
138
247
|
|
139
|
-
|
140
|
-
|
248
|
+
if in_v.is_a?(Array)
|
249
|
+
edge[:inVs] = in_v
|
250
|
+
else
|
251
|
+
edge[:inV] = in_v
|
252
|
+
end
|
141
253
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
254
|
+
edge[:document] = @document_id if label == 'item'
|
255
|
+
edge[:property] = property if property
|
256
|
+
|
257
|
+
@lsif_data << edge
|
258
|
+
@vertex_id += 1
|
259
|
+
@vertex_id - 1
|
260
|
+
end
|
261
|
+
|
262
|
+
def generate_hover_content(declaration)
|
263
|
+
case declaration[:type]
|
264
|
+
when :method
|
265
|
+
sig = []
|
266
|
+
sig << "def #{declaration[:name]}"
|
267
|
+
sig << "(#{declaration[:params]})" if declaration[:params]
|
268
|
+
sig.join
|
269
|
+
when :class
|
270
|
+
hover = ["class #{declaration[:name]}"]
|
271
|
+
hover << " < #{declaration[:superclass]}" if declaration[:superclass]
|
272
|
+
hover.join
|
273
|
+
when :module
|
274
|
+
"module #{declaration[:name]}"
|
275
|
+
when :constant
|
276
|
+
"#{declaration[:name]} = ..."
|
277
|
+
else
|
278
|
+
declaration[:name].to_s
|
147
279
|
end
|
148
280
|
end
|
149
281
|
|
282
|
+
def format_hover_data(data)
|
283
|
+
return data unless data[:contents]
|
284
|
+
|
285
|
+
data[:contents] = data[:contents].map do |content|
|
286
|
+
content[:value] = strip_code_block(content[:value])
|
287
|
+
content
|
288
|
+
end
|
289
|
+
data
|
290
|
+
end
|
291
|
+
|
292
|
+
def strip_code_block(text)
|
293
|
+
text.gsub(/```.*\n?/, '').strip
|
294
|
+
end
|
295
|
+
|
150
296
|
def valid_in_v?(in_v)
|
151
297
|
return false unless in_v
|
152
298
|
return in_v.any? if in_v.is_a?(Array)
|
153
|
-
|
154
299
|
true
|
155
300
|
end
|
156
301
|
|
157
|
-
def edge_document(label)
|
158
|
-
(label == 'item') ? @document_id : nil
|
159
|
-
end
|
160
|
-
|
161
302
|
def path_to_uri(path)
|
162
|
-
|
303
|
+
return nil unless path
|
304
|
+
normalized_path = path.gsub('\\', '/')
|
163
305
|
normalized_path = normalized_path.sub(%r{^file://}, '')
|
164
306
|
absolute_path = File.expand_path(normalized_path)
|
165
307
|
"file://#{absolute_path}"
|
166
308
|
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
309
|
end
|
182
310
|
end
|
183
311
|
end
|
@@ -1,17 +1,36 @@
|
|
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
|
+
|
17
|
+
# Method visibility tracking
|
18
|
+
@visibility_stack = [] # Stack of method visibility states per scope
|
19
|
+
@current_visibility = :public
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_declaration(qualified_name, data)
|
23
|
+
@declarations[qualified_name] = data
|
24
|
+
@result_sets[qualified_name] = data[:result_set_id] if data[:result_set_id]
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_reference(qualified_name, file_path, range_id, type = :reference)
|
28
|
+
@references[qualified_name] ||= []
|
29
|
+
@references[qualified_name] << {
|
30
|
+
file: file_path,
|
31
|
+
range_id: range_id,
|
32
|
+
type: type
|
33
|
+
}
|
15
34
|
end
|
16
35
|
|
17
36
|
def add_range(file_path, range_id)
|
@@ -19,13 +38,133 @@ module Hind
|
|
19
38
|
@ranges[file_path] << range_id
|
20
39
|
end
|
21
40
|
|
22
|
-
def
|
23
|
-
@
|
41
|
+
def has_declaration?(qualified_name)
|
42
|
+
@declarations.key?(qualified_name)
|
24
43
|
end
|
25
44
|
|
26
|
-
def
|
27
|
-
@
|
28
|
-
|
45
|
+
def get_declaration(qualified_name)
|
46
|
+
@declarations[qualified_name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_references(qualified_name)
|
50
|
+
@references[qualified_name] || []
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_result_set(qualified_name)
|
54
|
+
@result_sets[qualified_name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_ranges_for_file(file_path)
|
58
|
+
@ranges[file_path] || []
|
59
|
+
end
|
60
|
+
|
61
|
+
def push_visibility_scope(visibility = :public)
|
62
|
+
@visibility_stack.push(@current_visibility)
|
63
|
+
@current_visibility = visibility
|
64
|
+
end
|
65
|
+
|
66
|
+
def pop_visibility_scope
|
67
|
+
@current_visibility = @visibility_stack.pop || :public
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_visibility
|
71
|
+
@current_visibility
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_declaration_in_scope(name, scope)
|
75
|
+
# Try exact scope first
|
76
|
+
qualified_name = scope.empty? ? name : "#{scope}::#{name}"
|
77
|
+
return qualified_name if has_declaration?(qualified_name)
|
78
|
+
|
79
|
+
# Try parent scopes
|
80
|
+
scope_parts = scope.split('::')
|
81
|
+
while scope_parts.any?
|
82
|
+
scope_parts.pop
|
83
|
+
qualified_name = scope_parts.empty? ? name : "#{scope_parts.join('::')}::#{name}"
|
84
|
+
return qualified_name if has_declaration?(qualified_name)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Try top level
|
88
|
+
has_declaration?(name) ? name : nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_method_declaration(method_name, scope, instance_method = true)
|
92
|
+
separator = instance_method ? '#' : '.'
|
93
|
+
qualified_name = scope.empty? ? method_name : "#{scope}#{separator}#{method_name}"
|
94
|
+
|
95
|
+
return qualified_name if has_declaration?(qualified_name)
|
96
|
+
|
97
|
+
# For instance methods, try to find in superclass chain
|
98
|
+
if instance_method && !scope.empty?
|
99
|
+
current_scope = scope
|
100
|
+
while (class_data = @declarations[current_scope])
|
101
|
+
break unless class_data[:type] == :class && class_data[:superclass]
|
102
|
+
|
103
|
+
superclass = class_data[:superclass]
|
104
|
+
superclass_method = "#{superclass}#{separator}#{method_name}"
|
105
|
+
return superclass_method if has_declaration?(superclass_method)
|
106
|
+
|
107
|
+
current_scope = superclass
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def find_constant_declaration(name, current_scope)
|
115
|
+
return name if has_declaration?(name)
|
116
|
+
|
117
|
+
# Try with current scope
|
118
|
+
if current_scope && !current_scope.empty?
|
119
|
+
qualified_name = "#{current_scope}::#{name}"
|
120
|
+
return qualified_name if has_declaration?(qualified_name)
|
121
|
+
|
122
|
+
# Try parent scopes
|
123
|
+
scope_parts = current_scope.split('::')
|
124
|
+
while scope_parts.any?
|
125
|
+
scope_parts.pop
|
126
|
+
qualified_name = scope_parts.empty? ? name : "#{scope_parts.join('::')}::#{name}"
|
127
|
+
return qualified_name if has_declaration?(qualified_name)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Try top level
|
132
|
+
has_declaration?(name) ? name : nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def get_instance_variable_scope(var_name, current_scope)
|
136
|
+
return nil unless current_scope
|
137
|
+
"#{current_scope}##{var_name}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def get_class_variable_scope(var_name, current_scope)
|
141
|
+
return nil unless current_scope
|
142
|
+
"#{current_scope}::#{var_name}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def debug_info
|
146
|
+
{
|
147
|
+
declarations_count: @declarations.size,
|
148
|
+
references_count: @references.values.sum(&:size),
|
149
|
+
result_sets_count: @result_sets.size,
|
150
|
+
ranges_count: @ranges.values.sum(&:size),
|
151
|
+
declaration_types: declaration_types_count,
|
152
|
+
reference_types: reference_types_count
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def declaration_types_count
|
159
|
+
@declarations.values.each_with_object(Hash.new(0)) do |decl, counts|
|
160
|
+
counts[decl[:type]] += 1
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def reference_types_count
|
165
|
+
@references.values.flatten.each_with_object(Hash.new(0)) do |ref, counts|
|
166
|
+
counts[ref[:type]] += 1
|
167
|
+
end
|
29
168
|
end
|
30
169
|
end
|
31
170
|
end
|