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.
@@ -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