hind 0.1.4 → 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 +4 -2
- data/lib/hind/cli.rb +181 -64
- data/lib/hind/lsif/edge.rb +3 -1
- data/lib/hind/lsif/generator.rb +215 -86
- data/lib/hind/lsif/global_state.rb +152 -11
- data/lib/hind/lsif/vertex.rb +3 -1
- data/lib/hind/lsif/visitor.rb +211 -19
- data/lib/hind/lsif/visitors/declaration_visitor.rb +239 -0
- data/lib/hind/lsif/visitors/reference_visitor.rb +221 -0
- data/lib/hind/lsif.rb +6 -5
- data/lib/hind/parser.rb +3 -0
- data/lib/hind/scip/generator.rb +2 -0
- data/lib/hind/scip/visitor.rb +2 -0
- data/lib/hind/scip.rb +2 -0
- data/lib/hind/version.rb +1 -1
- data/lib/hind.rb +4 -4
- 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,7 +167,7 @@ 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
|
|
@@ -95,56 +175,122 @@ module Hind
|
|
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
|
|
194
|
+
ranges = @global_state.get_ranges_for_file(@current_uri)
|
113
195
|
if ranges&.any?
|
114
196
|
emit_edge('contains', @document_id, ranges)
|
115
197
|
end
|
116
198
|
end
|
117
199
|
|
118
|
-
def
|
119
|
-
@
|
120
|
-
definition = @global_state.definitions[qualified_name]
|
121
|
-
next unless definition
|
122
|
-
|
123
|
-
result_set_id = @global_state.result_sets[qualified_name]
|
124
|
-
next unless result_set_id
|
200
|
+
def create_range(start_location, end_location)
|
201
|
+
return nil unless @current_uri && start_location && end_location
|
125
202
|
|
126
|
-
|
127
|
-
|
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
|
+
})
|
128
213
|
|
129
|
-
|
130
|
-
|
131
|
-
|
214
|
+
@global_state.add_range(@current_uri, range_id)
|
215
|
+
range_id
|
216
|
+
end
|
132
217
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
218
|
+
def emit_vertex(label, data = nil)
|
219
|
+
vertex = {
|
220
|
+
id: @vertex_id,
|
221
|
+
type: 'vertex',
|
222
|
+
label: label
|
223
|
+
}
|
138
224
|
|
139
|
-
|
225
|
+
if data
|
226
|
+
if %w[hoverResult definitionResult referenceResult].include?(label)
|
227
|
+
vertex[:result] = format_hover_data(data)
|
228
|
+
else
|
229
|
+
vertex.merge!(data)
|
140
230
|
end
|
231
|
+
end
|
141
232
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
233
|
+
@lsif_data << vertex
|
234
|
+
@vertex_id += 1
|
235
|
+
@vertex_id - 1
|
236
|
+
end
|
237
|
+
|
238
|
+
def emit_edge(label, out_v, in_v, property = nil)
|
239
|
+
return unless out_v && valid_in_v?(in_v)
|
240
|
+
|
241
|
+
edge = {
|
242
|
+
id: @vertex_id,
|
243
|
+
type: 'edge',
|
244
|
+
label: label,
|
245
|
+
outV: out_v
|
246
|
+
}
|
247
|
+
|
248
|
+
if in_v.is_a?(Array)
|
249
|
+
edge[:inVs] = in_v
|
250
|
+
else
|
251
|
+
edge[:inV] = in_v
|
147
252
|
end
|
253
|
+
|
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
|
279
|
+
end
|
280
|
+
end
|
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
|
148
294
|
end
|
149
295
|
|
150
296
|
def valid_in_v?(in_v)
|
@@ -153,30 +299,13 @@ module Hind
|
|
153
299
|
true
|
154
300
|
end
|
155
301
|
|
156
|
-
def edge_document(label)
|
157
|
-
label == 'item' ? @document_id : nil
|
158
|
-
end
|
159
|
-
|
160
302
|
def path_to_uri(path)
|
303
|
+
return nil unless path
|
161
304
|
normalized_path = path.gsub('\\', '/')
|
162
305
|
normalized_path = normalized_path.sub(%r{^file://}, '')
|
163
306
|
absolute_path = File.expand_path(normalized_path)
|
164
307
|
"file://#{absolute_path}"
|
165
308
|
end
|
166
|
-
|
167
|
-
def make_hover_content(text)
|
168
|
-
{
|
169
|
-
contents: [{
|
170
|
-
language: 'ruby',
|
171
|
-
value: strip_code_block(text)
|
172
|
-
}]
|
173
|
-
}
|
174
|
-
end
|
175
|
-
|
176
|
-
def strip_code_block(text)
|
177
|
-
# Strip any existing code block markers and normalize
|
178
|
-
text.gsub(/```.*\n?/, '').strip
|
179
|
-
end
|
180
309
|
end
|
181
310
|
end
|
182
311
|
end
|
@@ -1,15 +1,36 @@
|
|
1
|
+
# lib/hind/lsif/global_state.rb
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
1
4
|
module Hind
|
2
5
|
module LSIF
|
3
6
|
class GlobalState
|
4
|
-
attr_reader :result_sets, :definitions, :references, :ranges
|
5
7
|
attr_accessor :project_id
|
8
|
+
attr_reader :declarations, :references, :result_sets, :ranges
|
6
9
|
|
7
10
|
def initialize
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@ranges = {}
|
12
|
-
@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
|
+
}
|
13
34
|
end
|
14
35
|
|
15
36
|
def add_range(file_path, range_id)
|
@@ -17,13 +38,133 @@ module Hind
|
|
17
38
|
@ranges[file_path] << range_id
|
18
39
|
end
|
19
40
|
|
20
|
-
def
|
21
|
-
@
|
41
|
+
def has_declaration?(qualified_name)
|
42
|
+
@declarations.key?(qualified_name)
|
22
43
|
end
|
23
44
|
|
24
|
-
def
|
25
|
-
@
|
26
|
-
|
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
|
27
168
|
end
|
28
169
|
end
|
29
170
|
end
|