hind 0.1.5 → 0.1.6
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 +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
|