hind 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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