hind 0.1.5 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fa041a7ffe7e916e1f3715500d21a9dea2a3a6ce60990be8cf4e7578f704702
4
- data.tar.gz: '0779f87c520ddc1edddf0448c96d596194982d5b87fa5c380bb3a1132500bd9e'
3
+ metadata.gz: 65e6158fb36117206f66c5989febbd5c336f7b8e88aa3b4a241a7d2cac091fa1
4
+ data.tar.gz: 753d17f81c708b80ebac94b1e17c1cfa2c34e4ed5bc767f9e2daed525b7485ab
5
5
  SHA512:
6
- metadata.gz: 397847759b6c959511602d276eee22f7e89959c5858d840cf8398b874a39ef6ccfeec8d876e9c0539d5cc0bc0cce77cb1e297b7fabc30c9a4562bfb7e85ddb7b
7
- data.tar.gz: 5c823067473c8b784a4868bf696b4374735b548eb8eb9341fb887dc78028e9223aeaac7172c621c624d0e8a3de221b50e101e908177626f9c316797e8ce2de70
6
+ metadata.gz: 649463b8b55e5016d822334667c1e61f6bc4fefa62230a5f7846f2b396f9ad467c8ff848d9637377c4851bda3d7bb068cc1a319a16d06b31a2269384bc6486c9
7
+ data.tar.gz: 0376aec80b6dbd5eba49244103d093a004e428e284e644b17109010a3175969f0df9eb261edc94fc60d071a8594a925aaa7ffa975af112c575b2781380e85beb
data/exe/hind CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'hind'
5
- require 'hind/cli'
4
+ require_relative '../lib/hind'
5
+ require_relative '../lib/hind/cli'
6
6
 
7
7
  Hind::CLI.start(ARGV)
data/lib/hind/cli.rb CHANGED
@@ -8,9 +8,8 @@ require 'fileutils'
8
8
  module Hind
9
9
  class CLI < Thor
10
10
  class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
11
- class_option :config, type: :string, aliases: '-c', desc: 'Path to configuration file'
12
11
 
13
- desc 'lsif', 'Generate LSIF index'
12
+ desc 'lsif', 'Generate LSIF index for Ruby classes, modules, and constants'
14
13
  method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
15
14
  method_option :output, type: :string, aliases: '-o', default: 'dump.lsif', desc: 'Output file path'
16
15
  method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
@@ -29,30 +28,7 @@ module Hind
29
28
  generate_lsif(files, options)
30
29
  say "\nLSIF data has been written to: #{options[:output]}", :green if options[:verbose]
31
30
  rescue => e
32
- abort "Error generating LSIF: #{e.message}"
33
- end
34
- end
35
-
36
- desc 'scip', 'Generate SCIP index'
37
- method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
38
- method_option :output, type: :string, aliases: '-o', default: 'index.scip', desc: 'Output file path'
39
- method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
40
- method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
41
- method_option :exclude, type: :array, aliases: '-e', desc: 'Patterns to exclude'
42
- def scip
43
- validate_directory(options[:directory])
44
- validate_output_file(options[:output], options[:force])
45
-
46
- files = find_files(options[:directory], options[:glob], options[:exclude])
47
- abort "No files found matching pattern '#{options[:glob]}'" if files.empty?
48
-
49
- say "Found #{files.length} files to process", :green if options[:verbose]
50
-
51
- begin
52
- generate_scip(files, options)
53
- say "\nSCIP data has been written to: #{options[:output]}", :green if options[:verbose]
54
- rescue => e
55
- abort "Error generating SCIP: #{e.message}"
31
+ handle_error(e, options[:verbose])
56
32
  end
57
33
  end
58
34
 
@@ -63,12 +39,65 @@ module Hind
63
39
 
64
40
  private
65
41
 
42
+ def generate_lsif(files, options)
43
+ # Initialize generator with absolute project root
44
+ generator = Hind::LSIF::Generator.new(
45
+ {
46
+ vertex_id: 1,
47
+ initial: true,
48
+ projectRoot: File.expand_path(options[:directory])
49
+ }
50
+ )
51
+
52
+ # Create file content map with relative paths
53
+ file_contents = {}
54
+ files.each do |file|
55
+ absolute_path = File.expand_path(file)
56
+ relative_path = Pathname.new(absolute_path)
57
+ .relative_path_from(Pathname.new(generator.metadata[:projectRoot]))
58
+ .to_s
59
+
60
+ begin
61
+ file_contents[relative_path] = File.read(absolute_path)
62
+ rescue => e
63
+ warn "Warning: Failed to read file '#{file}': #{e.message}"
64
+ next
65
+ end
66
+ end
67
+
68
+ File.open(options[:output], 'w') do |output_file|
69
+ say 'First pass: Collecting declarations...', :cyan if options[:verbose]
70
+
71
+ # First pass: Process all files to collect declarations
72
+ declaration_data = generator.collect_declarations(file_contents)
73
+
74
+ say "Found #{declaration_data[:declarations].size} declarations (classes, modules, constants)", :cyan if options[:verbose]
75
+ say 'Processing files for references...', :cyan if options[:verbose]
76
+
77
+ # Second pass: Process each file for references
78
+ file_contents.each do |relative_path, content|
79
+ if options[:verbose]
80
+ say "Processing file: #{relative_path}", :cyan
81
+ end
82
+
83
+ lsif_data = generator.process_file(
84
+ content: content,
85
+ uri: relative_path
86
+ )
87
+
88
+ output_file.puts(lsif_data.map(&:to_json).join("\n"))
89
+ end
90
+ end
91
+ end
92
+
66
93
  def validate_directory(directory)
67
94
  abort "Error: Directory '#{directory}' does not exist" unless Dir.exist?(directory)
68
95
  end
69
96
 
70
97
  def validate_output_file(output, force)
71
- abort "Error: Output file '#{output}' already exists. Use --force to overwrite." if File.exist?(output) && !force
98
+ if File.exist?(output) && !force
99
+ abort "Error: Output file '#{output}' already exists. Use --force to overwrite."
100
+ end
72
101
 
73
102
  # Ensure output directory exists
74
103
  FileUtils.mkdir_p(File.dirname(output))
@@ -85,53 +114,10 @@ module Hind
85
114
  files
86
115
  end
87
116
 
88
- def generate_lsif(files, options)
89
- global_state = Hind::LSIF::GlobalState.new
90
- vertex_id = 1
91
- initial = true
92
-
93
- File.open(options[:output], 'w') do |output_file|
94
- files.each do |file|
95
- say "Processing file: #{file}", :cyan if options[:verbose]
96
-
97
- relative_path = Pathname.new(file).relative_path_from(Pathname.new(options[:directory])).to_s
98
-
99
- begin
100
- generator = Hind::LSIF::Generator.new(
101
- {
102
- uri: relative_path,
103
- vertex_id: vertex_id,
104
- initial: initial,
105
- projectRoot: options[:directory]
106
- },
107
- global_state
108
- )
109
-
110
- output = generator.generate(File.read(file))
111
- vertex_id = output.last[:id].to_i + 1
112
- output_file.puts(output.map(&:to_json).join("\n"))
113
- initial = false
114
- rescue => e
115
- warn "Warning: Failed to process file '#{file}': #{e.message}"
116
- next
117
- end
118
- end
119
- end
120
- end
121
-
122
- def generate_scip(files, options)
123
- raise NotImplementedError, 'SCIP generation not yet implemented'
124
- # Similar to generate_lsif but using SCIP generator
125
- end
126
-
127
- def load_config(config_path)
128
- return {} unless config_path && File.exist?(config_path)
129
-
130
- begin
131
- YAML.load_file(config_path) || {}
132
- rescue => e
133
- abort "Error loading config file: #{e.message}"
134
- end
117
+ def handle_error(error, verbose)
118
+ message = "Error: #{error.message}"
119
+ message += "\n#{error.backtrace.join("\n")}" if verbose
120
+ abort message
135
121
  end
136
122
  end
137
123
  end
@@ -3,79 +3,127 @@
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 = {}
27
+ @current_document_id = nil
23
28
  @lsif_data = []
29
+ @current_uri = nil
24
30
 
25
31
  initialize_project if metadata[:initial]
26
32
  end
27
33
 
28
- def generate(code, file_metadata = {})
29
- @metadata = @metadata.merge(file_metadata)
34
+ def collect_declarations(files)
35
+ files.each do |path, content|
36
+ @current_uri = path
37
+ @document_id = nil
38
+ @current_document_id = nil
39
+
40
+ begin
41
+ ast = Parser.new(content).parse
42
+ setup_document
43
+ visitor = DeclarationVisitor.new(self, path)
44
+ visitor.visit(ast)
45
+ finalize_document_state
46
+ rescue => e
47
+ warn "Warning: Failed to collect declarations from '#{path}': #{e.message}"
48
+ end
49
+ end
50
+
51
+ {declarations: @global_state.declarations}
52
+ end
53
+
54
+ def process_file(params)
55
+ @current_uri = params[:uri]
56
+ content = params[:content]
57
+
58
+ @document_id = nil
59
+ @current_document_id = nil
60
+
30
61
  setup_document
62
+ ast = Parser.new(content).parse
31
63
 
32
- ast = Parser.new(code).parse
33
- visitor = Visitor.new(self)
64
+ visitor = ReferenceVisitor.new(self, @current_uri)
34
65
  visitor.visit(ast)
35
66
 
36
- finalize_document
37
- update_cross_file_references
38
-
39
- @lsif_data
67
+ result = @lsif_data
68
+ finalize_document_state
69
+ result
40
70
  end
41
71
 
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
- })
72
+ def register_declaration(declaration)
73
+ return unless @current_uri && declaration[:node]
53
74
 
54
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
55
- @global_state.add_range(file_path, range_id)
56
- range_id
57
- end
75
+ qualified_name = declaration[:name]
58
76
 
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
64
- end
77
+ setup_document if @document_id.nil?
78
+ current_doc_id = @document_id
65
79
 
66
- def emit_edge(label, out_v, in_v, property = nil)
67
- return unless out_v && valid_in_v?(in_v)
80
+ range_id = create_range(declaration[:node].location, declaration[:node].location)
81
+ return unless range_id
68
82
 
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
83
+ result_set_id = emit_vertex('resultSet')
84
+ emit_edge('next', range_id, result_set_id)
85
+
86
+ def_result_id = emit_vertex('definitionResult')
87
+ emit_edge('textDocument/definition', result_set_id, def_result_id)
88
+
89
+ emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
90
+
91
+ hover_content = generate_hover_content(declaration)
92
+ hover_id = emit_vertex('hoverResult', {
93
+ contents: [{
94
+ language: 'ruby',
95
+ value: hover_content
96
+ }]
97
+ })
98
+ emit_edge('textDocument/hover', result_set_id, hover_id)
99
+
100
+ @global_state.add_declaration(qualified_name, {
101
+ type: declaration[:type],
102
+ scope: declaration[:scope],
103
+ file: @current_uri,
104
+ range_id: range_id,
105
+ result_set_id: result_set_id,
106
+ document_id: current_doc_id
107
+ }.merge(declaration))
108
+
109
+ result_set_id
73
110
  end
74
111
 
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)
112
+ def register_reference(reference)
113
+ return unless @current_uri && reference[:node]
114
+ return unless @global_state.has_declaration?(reference[:name])
115
+
116
+ setup_document if @document_id.nil?
117
+ current_doc_id = @document_id
118
+
119
+ range_id = create_range(reference[:node].location, reference[:node].location)
120
+ return unless range_id
121
+
122
+ declaration = @global_state.declarations[reference[:name]]
123
+ return unless declaration[:result_set_id]
124
+
125
+ @global_state.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
126
+ emit_edge('next', range_id, declaration[:result_set_id])
79
127
  end
80
128
 
81
129
  private
@@ -87,7 +135,7 @@ module Hind
87
135
  positionEncoding: 'utf-16',
88
136
  toolInfo: {
89
137
  name: 'hind',
90
- version: VERSION
138
+ version: Hind::VERSION
91
139
  }
92
140
  })
93
141
 
@@ -95,89 +143,138 @@ module Hind
95
143
  end
96
144
 
97
145
  def setup_document
98
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
146
+ return if @document_id
147
+ return unless @current_uri
148
+
149
+ file_path = File.join(@metadata[:projectRoot], @current_uri)
99
150
 
100
151
  @document_id = emit_vertex('document', {
101
152
  uri: path_to_uri(file_path),
102
153
  languageId: 'ruby'
103
154
  })
104
- @document_ids[file_path] = @document_id
155
+
156
+ @document_ids[@current_uri] = @document_id
157
+ @current_document_id = @document_id
105
158
 
106
159
  emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
107
160
  end
108
161
 
109
- def finalize_document
110
- file_path = File.join(@metadata[:projectRoot], @metadata[:uri])
111
- ranges = @global_state.ranges[file_path]
162
+ def finalize_document_state
163
+ return unless @current_uri && @document_id
164
+
165
+ ranges = @global_state.get_ranges_for_file(@current_uri)
166
+ if ranges&.any?
167
+ emit_edge('contains', @document_id, ranges, nil, @document_id)
168
+ end
169
+ end
112
170
 
113
- return unless ranges&.any?
171
+ def create_range(start_location, end_location)
172
+ return nil unless @current_uri && start_location && end_location
114
173
 
115
- emit_edge('contains', @document_id, ranges)
174
+ range_id = emit_vertex('range', {
175
+ start: {
176
+ line: start_location.start_line - 1,
177
+ character: start_location.start_column
178
+ },
179
+ end: {
180
+ line: end_location.end_line - 1,
181
+ character: end_location.end_column
182
+ }
183
+ })
184
+
185
+ @global_state.add_range(@current_uri, range_id)
186
+ range_id
116
187
  end
117
188
 
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
189
+ def emit_vertex(label, data = nil)
190
+ vertex = {
191
+ id: @vertex_id,
192
+ type: 'vertex',
193
+ label: label
194
+ }
195
+
196
+ if data
197
+ if %w[hoverResult definitionResult referenceResult].include?(label)
198
+ vertex[:result] = format_hover_data(data)
199
+ else
200
+ vertex.merge!(data)
201
+ end
202
+ end
122
203
 
123
- result_set_id = @global_state.result_sets[qualified_name]
124
- next unless result_set_id
204
+ @lsif_data << vertex
205
+ @vertex_id += 1
206
+ @vertex_id - 1
207
+ end
125
208
 
126
- ref_result_id = emit_vertex('referenceResult')
127
- emit_edge('textDocument/references', result_set_id, ref_result_id)
209
+ def emit_edge(label, out_v, in_v, property = nil, doc_id = nil)
210
+ return unless out_v && valid_in_v?(in_v)
128
211
 
129
- # Collect all reference range IDs
130
- all_refs = references.map { |ref| ref[:range_id] }
131
- all_refs << definition[:range_id]
212
+ edge = {
213
+ id: @vertex_id,
214
+ type: 'edge',
215
+ label: label,
216
+ outV: out_v
217
+ }
132
218
 
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
219
+ if in_v.is_a?(Array)
220
+ edge[:inVs] = in_v
221
+ else
222
+ edge[:inV] = in_v
223
+ end
138
224
 
139
- emit_edge('item', ref_result_id, all_refs, 'references')
140
- end
225
+ if label == 'item'
226
+ edge[:document] = doc_id || @current_document_id
227
+ edge[:property] = property if property
228
+ end
141
229
 
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
230
+ @lsif_data << edge
231
+ @vertex_id += 1
232
+ @vertex_id - 1
233
+ end
234
+
235
+ def generate_hover_content(declaration)
236
+ case declaration[:type]
237
+ when :class
238
+ hover = ["class #{declaration[:name]}"]
239
+ hover << " < #{declaration[:superclass]}" if declaration[:superclass]
240
+ hover.join
241
+ when :module
242
+ "module #{declaration[:name]}"
243
+ when :constant
244
+ value_info = declaration[:node].value ? " = #{declaration[:node].value.inspect}" : ''
245
+ "#{declaration[:name]}#{value_info}"
246
+ else
247
+ declaration[:name].to_s
248
+ end
249
+ end
250
+
251
+ def format_hover_data(data)
252
+ return data unless data[:contents]
253
+
254
+ data[:contents] = data[:contents].map do |content|
255
+ content[:value] = strip_code_block(content[:value])
256
+ content
147
257
  end
258
+ data
259
+ end
260
+
261
+ def strip_code_block(text)
262
+ text.gsub(/```.*\n?/, '').strip
148
263
  end
149
264
 
150
265
  def valid_in_v?(in_v)
151
266
  return false unless in_v
152
267
  return in_v.any? if in_v.is_a?(Array)
153
-
154
268
  true
155
269
  end
156
270
 
157
- def edge_document(label)
158
- (label == 'item') ? @document_id : nil
159
- end
160
-
161
271
  def path_to_uri(path)
272
+ return nil unless path
162
273
  normalized_path = path.tr('\\', '/')
163
274
  normalized_path = normalized_path.sub(%r{^file://}, '')
164
275
  absolute_path = File.expand_path(normalized_path)
165
276
  "file://#{absolute_path}"
166
277
  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
278
  end
182
279
  end
183
280
  end
@@ -1,17 +1,32 @@
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
+ end
17
+
18
+ def add_declaration(qualified_name, data)
19
+ @declarations[qualified_name] = data
20
+ @result_sets[qualified_name] = data[:result_set_id] if data[:result_set_id]
21
+ end
22
+
23
+ def add_reference(qualified_name, file_path, range_id, document_id)
24
+ @references[qualified_name] ||= []
25
+ @references[qualified_name] << {
26
+ file: file_path,
27
+ range_id: range_id,
28
+ document_id: document_id
29
+ }
15
30
  end
16
31
 
17
32
  def add_range(file_path, range_id)
@@ -19,13 +34,60 @@ module Hind
19
34
  @ranges[file_path] << range_id
20
35
  end
21
36
 
22
- def add_definition(qualified_name, file_path, range_id)
23
- @definitions[qualified_name] = {file: file_path, range_id: range_id}
37
+ def has_declaration?(qualified_name)
38
+ @declarations.key?(qualified_name)
24
39
  end
25
40
 
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}
41
+ def get_declaration(qualified_name)
42
+ @declarations[qualified_name]
43
+ end
44
+
45
+ def get_references(qualified_name)
46
+ @references[qualified_name] || []
47
+ end
48
+
49
+ def get_result_set(qualified_name)
50
+ @result_sets[qualified_name]
51
+ end
52
+
53
+ def get_ranges_for_file(file_path)
54
+ @ranges[file_path] || []
55
+ end
56
+
57
+ def find_constant_declaration(name, current_scope)
58
+ return name if has_declaration?(name)
59
+
60
+ if current_scope && !current_scope.empty?
61
+ qualified_name = "#{current_scope}::#{name}"
62
+ return qualified_name if has_declaration?(qualified_name)
63
+
64
+ scope_parts = current_scope.split('::')
65
+ while scope_parts.any?
66
+ scope_parts.pop
67
+ qualified_name = scope_parts.empty? ? name : "#{scope_parts.join("::")}::#{name}"
68
+ return qualified_name if has_declaration?(qualified_name)
69
+ end
70
+ end
71
+
72
+ has_declaration?(name) ? name : nil
73
+ end
74
+
75
+ def debug_info
76
+ {
77
+ declarations_count: @declarations.size,
78
+ references_count: @references.values.sum(&:size),
79
+ result_sets_count: @result_sets.size,
80
+ ranges_count: @ranges.values.sum(&:size),
81
+ declaration_types: declaration_types_count
82
+ }
83
+ end
84
+
85
+ private
86
+
87
+ def declaration_types_count
88
+ @declarations.values.each_with_object(Hash.new(0)) do |decl, counts|
89
+ counts[decl[:type]] += 1
90
+ end
29
91
  end
30
92
  end
31
93
  end
@@ -0,0 +1,69 @@
1
+ # lib/hind/lsif/visitors/declaration_visitor.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Hind
5
+ module LSIF
6
+ class DeclarationVisitor < Prism::Visitor
7
+ attr_reader :current_scope
8
+
9
+ def initialize(generator, file_path)
10
+ @generator = generator
11
+ @file_path = file_path
12
+ @current_scope = []
13
+ end
14
+
15
+ def visit_class_node(node)
16
+ @current_scope.push(node.constant_path.slice)
17
+ class_name = current_scope_name
18
+
19
+ @generator.register_declaration({
20
+ type: :class,
21
+ name: class_name,
22
+ node: node,
23
+ scope: @current_scope[0..-2].join('::'),
24
+ superclass: node.superclass&.slice
25
+ })
26
+
27
+ super
28
+ @current_scope.pop
29
+ end
30
+
31
+ def visit_module_node(node)
32
+ @current_scope.push(node.constant_path.slice)
33
+ module_name = current_scope_name
34
+
35
+ @generator.register_declaration({
36
+ type: :module,
37
+ name: module_name,
38
+ node: node,
39
+ scope: @current_scope[0..-2].join('::')
40
+ })
41
+
42
+ super
43
+ @current_scope.pop
44
+ end
45
+
46
+ def visit_constant_write_node(node)
47
+ return unless node.name
48
+
49
+ constant_name = node.name.to_s
50
+ qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
51
+
52
+ @generator.register_declaration({
53
+ type: :constant,
54
+ name: qualified_name,
55
+ node: node,
56
+ scope: current_scope_name
57
+ })
58
+
59
+ super
60
+ end
61
+
62
+ private
63
+
64
+ def current_scope_name
65
+ @current_scope.join('::')
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hind
4
+ module LSIF
5
+ class ReferenceVisitor < Prism::Visitor
6
+ attr_reader :current_scope
7
+
8
+ def initialize(generator, file_path)
9
+ @generator = generator
10
+ @file_path = file_path
11
+ @current_scope = []
12
+ end
13
+
14
+ def visit_constant_read_node(node)
15
+ return unless node.name
16
+
17
+ constant_name = node.name.to_s
18
+ qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
19
+
20
+ @generator.register_reference({
21
+ type: :constant,
22
+ name: qualified_name,
23
+ node: node,
24
+ scope: current_scope_name
25
+ })
26
+
27
+ super
28
+ end
29
+
30
+ def visit_constant_path_node(node)
31
+ qualified_name = node.slice
32
+
33
+ @generator.register_reference({
34
+ type: :constant,
35
+ name: qualified_name,
36
+ node: node,
37
+ scope: current_scope_name
38
+ })
39
+
40
+ super
41
+ end
42
+
43
+ def visit_class_node(node)
44
+ @current_scope.push(node.constant_path.slice)
45
+ super
46
+ @current_scope.pop
47
+ end
48
+
49
+ def visit_module_node(node)
50
+ @current_scope.push(node.constant_path.slice)
51
+ super
52
+ @current_scope.pop
53
+ end
54
+
55
+ private
56
+
57
+ def current_scope_name
58
+ @current_scope.join('::')
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/hind/lsif.rb CHANGED
@@ -4,7 +4,6 @@ require_relative 'lsif/global_state'
4
4
  require_relative 'lsif/edge'
5
5
  require_relative 'lsif/generator'
6
6
  require_relative 'lsif/vertex'
7
- require_relative 'lsif/visitor'
8
7
 
9
8
  module Hind
10
9
  module LSIF
data/lib/hind/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hind
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.7'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hind
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aboobacker MK
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-09 00:00:00.000000000 Z
10
+ date: 2025-02-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: prism
@@ -111,7 +111,8 @@ files:
111
111
  - lib/hind/lsif/generator.rb
112
112
  - lib/hind/lsif/global_state.rb
113
113
  - lib/hind/lsif/vertex.rb
114
- - lib/hind/lsif/visitor.rb
114
+ - lib/hind/lsif/visitors/declaration_visitor.rb
115
+ - lib/hind/lsif/visitors/reference_visitor.rb
115
116
  - lib/hind/parser.rb
116
117
  - lib/hind/scip.rb
117
118
  - lib/hind/scip/generator.rb
@@ -1,268 +0,0 @@
1
- # lib/hind/lsif/visitor.rb
2
- # frozen_string_literal: true
3
-
4
- module Hind
5
- module LSIF
6
- class Visitor
7
- def initialize(generator)
8
- @generator = generator
9
- @current_scope = []
10
- end
11
-
12
- def visit(node)
13
- return unless node
14
-
15
- method_name = "visit_#{node.class.name.split("::").last.downcase}"
16
- if respond_to?(method_name)
17
- send(method_name, node)
18
- else
19
- visit_children(node)
20
- end
21
- end
22
-
23
- def visit_children(node)
24
- node.child_nodes.each { |child| visit(child) if child }
25
- end
26
-
27
- def visit_defnode(node)
28
- # Handle method definitions
29
- method_name = node.name.to_s
30
- qualified_name = current_scope_name.empty? ? method_name : "#{current_scope_name}##{method_name}"
31
-
32
- range_id = @generator.create_range(node.location, node.location)
33
- result_set_id = @generator.emit_vertex('resultSet')
34
- @generator.emit_edge('next', range_id, result_set_id)
35
-
36
- def_result_id = @generator.emit_vertex('definitionResult')
37
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
38
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
39
-
40
- # Generate method signature for hover
41
- sig = []
42
- sig << "def #{qualified_name}"
43
- sig << "(#{node.parameters.slice})" if node.parameters
44
-
45
- hover_id = @generator.emit_vertex('hoverResult', {
46
- contents: [{
47
- language: 'ruby',
48
- value: sig.join
49
- }]
50
- })
51
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
52
-
53
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
54
-
55
- visit_children(node)
56
- end
57
-
58
- def visit_classnode(node)
59
- @current_scope.push(node.constant_path.slice)
60
- class_name = current_scope_name
61
-
62
- range_id = @generator.create_range(node.location, node.location)
63
- result_set_id = @generator.emit_vertex('resultSet')
64
- @generator.emit_edge('next', range_id, result_set_id)
65
-
66
- def_result_id = @generator.emit_vertex('definitionResult')
67
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
68
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
69
-
70
- # Generate hover with inheritance info
71
- hover = []
72
- class_def = "class #{class_name}"
73
- class_def += " < #{node.superclass.slice}" if node.superclass
74
-
75
- hover << if node.superclass
76
- "#{class_def}\n\nInherits from: #{node.superclass.slice}"
77
- else
78
- class_def
79
- end
80
-
81
- hover_id = @generator.emit_vertex('hoverResult', {
82
- contents: [{
83
- language: 'ruby',
84
- value: hover.join("\n")
85
- }]
86
- })
87
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
88
-
89
- @generator.add_to_global_state(class_name, result_set_id, range_id)
90
-
91
- # Handle inheritance
92
- visit_inheritance(node.superclass) if node.superclass
93
-
94
- visit_children(node)
95
- @current_scope.pop
96
- end
97
-
98
- def visit_modulenode(node)
99
- @current_scope.push(node.constant_path.slice)
100
- module_name = current_scope_name
101
-
102
- range_id = @generator.create_range(node.location, node.location)
103
- result_set_id = @generator.emit_vertex('resultSet')
104
- @generator.emit_edge('next', range_id, result_set_id)
105
-
106
- def_result_id = @generator.emit_vertex('definitionResult')
107
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
108
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
109
-
110
- hover_id = @generator.emit_vertex('hoverResult', {
111
- contents: [{
112
- language: 'ruby',
113
- value: "module #{module_name}"
114
- }]
115
- })
116
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
117
-
118
- @generator.add_to_global_state(module_name, result_set_id, range_id)
119
-
120
- visit_children(node)
121
- @current_scope.pop
122
- end
123
-
124
- def visit_callnode(node)
125
- return unless node.name && node.location
126
-
127
- method_name = node.name.to_s
128
- qualified_names = []
129
-
130
- # Try with current scope first
131
- qualified_names << "#{current_scope_name}##{method_name}" unless current_scope_name.empty?
132
-
133
- # Try with receiver's type if available
134
- if node.receiver
135
- case node.receiver
136
- when Prism::ConstantReadNode
137
- qualified_names << "#{node.receiver.name}##{method_name}"
138
- when Prism::CallNode
139
- # Handle method chaining
140
- qualified_names << "#{node.receiver.name}##{method_name}" if node.receiver.name
141
- when Prism::InstanceVariableReadNode
142
- # Handle instance variable calls
143
- qualified_names << "#{current_scope_name}##{method_name}" if current_scope_name
144
- end
145
- end
146
-
147
- # Try as a standalone method
148
- qualified_names << method_name
149
-
150
- # Add references for matching qualified names
151
- qualified_names.each do |qualified_name|
152
- next unless @generator.global_state.result_sets[qualified_name]
153
-
154
- range_id = @generator.create_range(node.location, node.location)
155
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
156
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
157
- break # Stop after finding first match
158
- end
159
-
160
- visit_children(node)
161
- end
162
-
163
- def visit_constantreadnode(node)
164
- return unless node.name
165
-
166
- constant_name = node.name.to_s
167
- qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
168
-
169
- return unless @generator.global_state.result_sets[qualified_name]
170
-
171
- range_id = @generator.create_range(node.location, node.location)
172
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
173
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
174
- end
175
-
176
- def visit_constantwritenode(node)
177
- return unless node.name
178
-
179
- constant_name = node.name.to_s
180
- qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
181
-
182
- range_id = @generator.create_range(node.location, node.location)
183
- result_set_id = @generator.emit_vertex('resultSet')
184
- @generator.emit_edge('next', range_id, result_set_id)
185
-
186
- def_result_id = @generator.emit_vertex('definitionResult')
187
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
188
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
189
-
190
- hover_id = @generator.emit_vertex('hoverResult', {
191
- contents: [{
192
- language: 'ruby',
193
- value: "#{qualified_name} = ..."
194
- }]
195
- })
196
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
197
-
198
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
199
-
200
- visit_children(node)
201
- end
202
-
203
- def visit_instancevariablereadnode(node)
204
- return unless node.name && current_scope_name
205
-
206
- var_name = node.name.to_s
207
- qualified_name = "#{current_scope_name}##{var_name}"
208
-
209
- return unless @generator.global_state.result_sets[qualified_name]
210
-
211
- range_id = @generator.create_range(node.location, node.location)
212
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
213
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
214
- end
215
-
216
- def visit_instancevariablewritenode(node)
217
- return unless node.name && current_scope_name
218
-
219
- var_name = node.name.to_s
220
- qualified_name = "#{current_scope_name}##{var_name}"
221
-
222
- range_id = @generator.create_range(node.location, node.location)
223
- result_set_id = @generator.emit_vertex('resultSet')
224
- @generator.emit_edge('next', range_id, result_set_id)
225
-
226
- def_result_id = @generator.emit_vertex('definitionResult')
227
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
228
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
229
-
230
- hover_id = @generator.emit_vertex('hoverResult', {
231
- contents: [{
232
- language: 'ruby',
233
- value: "Instance variable #{var_name} in #{current_scope_name}"
234
- }]
235
- })
236
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
237
-
238
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
239
-
240
- visit_children(node)
241
- end
242
-
243
- private
244
-
245
- def current_scope_name
246
- @current_scope.join('::')
247
- end
248
-
249
- def visit_inheritance(node)
250
- case node
251
- when Prism::ConstantReadNode, Prism::ConstantPathNode
252
- range_id = @generator.create_range(node.location, node.location)
253
- qualified_name = case node
254
- when Prism::ConstantReadNode
255
- node.name.to_s
256
- when Prism::ConstantPathNode
257
- node.slice
258
- end
259
-
260
- return unless @generator.global_state.result_sets[qualified_name]
261
-
262
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
263
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
264
- end
265
- end
266
- end
267
- end
268
- end