hind 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, :metadata
16
+ attr_reader :metadata, :global_state, :document_id, :current_uri
13
17
 
14
- def initialize(metadata = {}, global_state = nil)
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 = global_state || GlobalState.new
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 generate(code, file_metadata = {})
29
- @metadata = @metadata.merge(file_metadata)
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
- ast = Parser.new(code).parse
33
- visitor = Visitor.new(self)
51
+ # Process declarations first to update any missing ones
52
+ visitor = DeclarationVisitor.new(self, @current_uri)
34
53
  visitor.visit(ast)
35
54
 
36
- finalize_document
37
- update_cross_file_references
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 create_range(start_location, end_location)
43
- range_id = emit_vertex('range', {
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
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
55
- @global_state.add_range(file_path, range_id)
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
- def emit_vertex(label, data = nil)
60
- vertex = Vertex.new(@vertex_id, label, data)
61
- @lsif_data << vertex.to_json
62
- @vertex_id += 1
63
- @vertex_id - 1
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 emit_edge(label, out_v, in_v, property = nil)
67
- return unless out_v && valid_in_v?(in_v)
95
+ def register_reference(reference)
96
+ return unless @current_uri && reference[:node]
97
+ return unless @global_state.has_declaration?(reference[:name])
68
98
 
69
- edge = Edge.new(@vertex_id, label, out_v, in_v, property, edge_document(label))
70
- @lsif_data << edge.to_json
71
- @vertex_id += 1
72
- @vertex_id - 1
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 add_to_global_state(qualified_name, result_set_id, range_id)
76
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
77
- @global_state.result_sets[qualified_name] = result_set_id
78
- @global_state.add_definition(qualified_name, file_path, range_id)
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
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
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[file_path] = @document_id
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
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
111
- ranges = @global_state.ranges[file_path]
192
+ return unless @current_uri
112
193
 
113
- return unless ranges&.any?
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
- emit_edge('contains', @document_id, ranges)
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 update_cross_file_references
119
- @global_state.references.each do |qualified_name, references|
120
- definition = @global_state.definitions[qualified_name]
121
- next unless definition
218
+ def emit_vertex(label, data = nil)
219
+ vertex = {
220
+ id: @vertex_id,
221
+ type: 'vertex',
222
+ label: label
223
+ }
122
224
 
123
- result_set_id = @global_state.result_sets[qualified_name]
124
- next unless result_set_id
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
- ref_result_id = emit_vertex('referenceResult')
127
- emit_edge('textDocument/references', result_set_id, ref_result_id)
233
+ @lsif_data << vertex
234
+ @vertex_id += 1
235
+ @vertex_id - 1
236
+ end
128
237
 
129
- # Collect all reference range IDs
130
- all_refs = references.map { |ref| ref[:range_id] }
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
- # Group references by document
134
- reference_documents = references.group_by { |ref| ref[:file] }
135
- reference_documents.each_key do |file_path|
136
- document_id = @document_ids[file_path]
137
- next unless document_id
241
+ edge = {
242
+ id: @vertex_id,
243
+ type: 'edge',
244
+ label: label,
245
+ outV: out_v
246
+ }
138
247
 
139
- emit_edge('item', ref_result_id, all_refs, 'references')
140
- end
248
+ if in_v.is_a?(Array)
249
+ edge[:inVs] = in_v
250
+ else
251
+ edge[:inV] = in_v
252
+ end
141
253
 
142
- # Handle definition document if not already included
143
- def_document_id = @document_ids[definition[:file]]
144
- if def_document_id && references.none? { |ref| ref[:file] == definition[:file] }
145
- emit_edge('item', ref_result_id, all_refs, 'references')
146
- end
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
- normalized_path = path.tr('\\', '/')
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
- @result_sets = {} # {qualified_name => result_set_id}
11
- @definitions = {} # {qualified_name => {file: file_path, range_id: id}}
12
- @references = {} # {qualified_name => [{file: file_path, range_id: id}]}
13
- @ranges = {} # {file_path => [range_ids]}
14
- @project_id = nil # Store project ID for reuse across files
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 add_definition(qualified_name, file_path, range_id)
23
- @definitions[qualified_name] = {file: file_path, range_id: range_id}
41
+ def has_declaration?(qualified_name)
42
+ @declarations.key?(qualified_name)
24
43
  end
25
44
 
26
- def add_reference(qualified_name, file_path, range_id)
27
- @references[qualified_name] ||= []
28
- @references[qualified_name] << {file: file_path, range_id: range_id}
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