hind 0.1.6 → 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: 66f58f99f053b938815d051ff6404a478a4f340178f74556ce6ab63f0a93c2e4
4
- data.tar.gz: 02e04a6c88687f9baf37013fc216dc7df52b5fa6e81f60170cbb31146335c91b
3
+ metadata.gz: 65e6158fb36117206f66c5989febbd5c336f7b8e88aa3b4a241a7d2cac091fa1
4
+ data.tar.gz: 753d17f81c708b80ebac94b1e17c1cfa2c34e4ed5bc767f9e2daed525b7485ab
5
5
  SHA512:
6
- metadata.gz: 529d648a5f0677180c4c73daa8d316661f4b82b7bd4802318c8bb42a7ed7bd8d2ffb38b69a82a274483638ea7bc1c161ea9d6bb6f3c7f84e3ff75464a1968c75
7
- data.tar.gz: dc734f7623cffa1c0cc6260627d14916e6e9809c3b06afcad1bd87015c8c32b8bbe979ed978abb922adc3af7a19c7c3099f3217c00016711260aacd5079fab6c
6
+ metadata.gz: 649463b8b55e5016d822334667c1e61f6bc4fefa62230a5f7846f2b396f9ad467c8ff848d9637377c4851bda3d7bb068cc1a319a16d06b31a2269384bc6486c9
7
+ data.tar.gz: 0376aec80b6dbd5eba49244103d093a004e428e284e644b17109010a3175969f0df9eb261edc94fc60d071a8594a925aaa7ffa975af112c575b2781380e85beb
data/lib/hind/cli.rb CHANGED
@@ -4,59 +4,30 @@ require 'thor'
4
4
  require 'json'
5
5
  require 'pathname'
6
6
  require 'fileutils'
7
- require 'yaml'
8
7
 
9
8
  module Hind
10
9
  class CLI < Thor
11
10
  class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
12
- class_option :config, type: :string, aliases: '-c', desc: 'Path to configuration file'
13
11
 
14
- desc 'lsif', 'Generate LSIF index'
12
+ desc 'lsif', 'Generate LSIF index for Ruby classes, modules, and constants'
15
13
  method_option :directory, type: :string, aliases: '-d', default: '.', desc: 'Root directory to process'
16
14
  method_option :output, type: :string, aliases: '-o', default: 'dump.lsif', desc: 'Output file path'
17
15
  method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
18
16
  method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
19
17
  method_option :exclude, type: :array, aliases: '-e', desc: 'Patterns to exclude'
20
- method_option :workers, type: :numeric, aliases: '-w', default: 1, desc: 'Number of parallel workers'
21
18
  def lsif
22
- config = load_config(options[:config])
23
- opts = config.merge(symbolize_keys(options))
19
+ validate_directory(options[:directory])
20
+ validate_output_file(options[:output], options[:force])
24
21
 
25
- validate_directory(opts[:directory])
26
- validate_output_file(opts[:output], opts[:force])
22
+ files = find_files(options[:directory], options[:glob], options[:exclude])
23
+ abort "No files found matching pattern '#{options[:glob]}'" if files.empty?
27
24
 
28
- files = find_files(opts[:directory], opts[:glob], opts[:exclude])
29
- abort "No files found matching pattern '#{opts[:glob]}'" if files.empty?
30
-
31
- say "Found #{files.length} files to process", :green if opts[:verbose]
32
-
33
- begin
34
- generate_lsif(files, opts)
35
- say "\nLSIF data has been written to: #{opts[:output]}", :green if opts[:verbose]
36
- rescue StandardError => e
37
- handle_error(e, opts[:verbose])
38
- end
39
- end
40
-
41
- desc 'check', 'Check LSIF dump file for validity and provide insights'
42
- method_option :file, type: :string, aliases: '-f', default: 'dump.lsif', desc: 'LSIF dump file to check'
43
- method_option :json, type: :boolean, desc: 'Output results in JSON format'
44
- method_option :strict, type: :boolean, desc: 'Treat warnings as errors'
45
- def check
46
- abort "Error: File '#{options[:file]}' does not exist" unless File.exist?(options[:file])
25
+ say "Found #{files.length} files to process", :green if options[:verbose]
47
26
 
48
27
  begin
49
- checker = Hind::LSIF::Checker.new(options[:file])
50
- results = checker.check
51
-
52
- if options[:json]
53
- puts JSON.pretty_generate(results)
54
- else
55
- print_check_results(results)
56
- end
57
-
58
- exit(1) if !results[:valid] || (options[:strict] && results[:warnings].any?)
59
- rescue StandardError => e
28
+ generate_lsif(files, options)
29
+ say "\nLSIF data has been written to: #{options[:output]}", :green if options[:verbose]
30
+ rescue => e
60
31
  handle_error(e, options[:verbose])
61
32
  end
62
33
  end
@@ -66,18 +37,6 @@ module Hind
66
37
  say "Hind version #{Hind::VERSION}"
67
38
  end
68
39
 
69
- desc 'init', 'Initialize Hind configuration file'
70
- method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite existing configuration'
71
- def init
72
- config_file = '.hind.yml'
73
- if File.exist?(config_file) && !options[:force]
74
- abort "Configuration file already exists. Use --force to overwrite."
75
- end
76
-
77
- create_default_config(config_file)
78
- say "Created configuration file: #{config_file}", :green
79
- end
80
-
81
40
  private
82
41
 
83
42
  def generate_lsif(files, options)
@@ -95,24 +54,27 @@ module Hind
95
54
  files.each do |file|
96
55
  absolute_path = File.expand_path(file)
97
56
  relative_path = Pathname.new(absolute_path)
98
- .relative_path_from(Pathname.new(generator.metadata[:projectRoot]))
99
- .to_s
100
- file_contents[relative_path] = File.read(absolute_path)
101
- rescue StandardError => e
102
- warn "Warning: Failed to read file '#{file}': #{e.message}"
103
- next
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
104
66
  end
105
67
 
106
68
  File.open(options[:output], 'w') do |output_file|
107
- say "First pass: Collecting declarations...", :cyan if options[:verbose]
69
+ say 'First pass: Collecting declarations...', :cyan if options[:verbose]
108
70
 
109
71
  # First pass: Process all files to collect declarations
110
72
  declaration_data = generator.collect_declarations(file_contents)
111
73
 
112
- say "Found #{declaration_data[:declarations].size} declarations", :cyan if options[:verbose]
113
- say "Processing files...", :cyan if options[:verbose]
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]
114
76
 
115
- # Second pass: Process each file
77
+ # Second pass: Process each file for references
116
78
  file_contents.each do |relative_path, content|
117
79
  if options[:verbose]
118
80
  say "Processing file: #{relative_path}", :cyan
@@ -125,11 +87,6 @@ module Hind
125
87
 
126
88
  output_file.puts(lsif_data.map(&:to_json).join("\n"))
127
89
  end
128
-
129
- # Write cross-reference data
130
- say "Finalizing cross-references...", :cyan if options[:verbose]
131
- cross_refs = generator.finalize_cross_references
132
- output_file.puts(cross_refs.map(&:to_json).join("\n")) if cross_refs&.any?
133
90
  end
134
91
  end
135
92
 
@@ -150,109 +107,17 @@ module Hind
150
107
  pattern = File.join(directory, glob)
151
108
  files = Dir.glob(pattern)
152
109
 
153
- if exclude_patterns
154
- exclude_patterns.each do |exclude|
155
- files.reject! { |f| File.fnmatch?(exclude, f) }
156
- end
110
+ exclude_patterns&.each do |exclude|
111
+ files.reject! { |f| File.fnmatch?(exclude, f) }
157
112
  end
158
113
 
159
114
  files
160
115
  end
161
116
 
162
- def load_config(config_path)
163
- return {} unless config_path && File.exist?(config_path)
164
-
165
- begin
166
- YAML.load_file(config_path) || {}
167
- rescue StandardError => e
168
- abort "Error loading config file: #{e.message}"
169
- end
170
- end
171
-
172
- def create_default_config(config_file)
173
- config = {
174
- 'directory' => '.',
175
- 'output' => 'dump.lsif',
176
- 'glob' => '**/*.rb',
177
- 'exclude' => [
178
- 'test/**/*',
179
- 'spec/**/*',
180
- 'vendor/**/*'
181
- ],
182
- 'workers' => 1
183
- }
184
-
185
- File.write(config_file, config.to_yaml)
186
- end
187
-
188
- def print_check_results(results)
189
- print_check_status(results[:valid])
190
- print_check_errors(results[:errors])
191
- print_check_warnings(results[:warnings])
192
- print_check_statistics(results[:statistics])
193
- end
194
-
195
- def print_check_status(valid)
196
- status = valid ? "✅ LSIF dump is valid" : "❌ LSIF dump contains errors"
197
- say(status, valid ? :green : :red)
198
- puts
199
- end
200
-
201
- def print_check_errors(errors)
202
- return if errors.empty?
203
-
204
- say "Errors:", :red
205
- errors.each do |error|
206
- say " • #{error}", :red
207
- end
208
- puts
209
- end
210
-
211
- def print_check_warnings(warnings)
212
- return if warnings.empty?
213
-
214
- say "Warnings:", :yellow
215
- warnings.each do |warning|
216
- say " • #{warning}", :yellow
217
- end
218
- puts
219
- end
220
-
221
- def print_check_statistics(stats)
222
- say "Statistics:", :cyan
223
- say " Total Elements: #{stats[:total_elements]}"
224
- say " Vertices: #{stats[:vertices][:total]}"
225
- say " Edges: #{stats[:edges][:total]}"
226
- say " Vertex/Edge Ratio: #{stats[:vertex_to_edge_ratio]}"
227
- puts
228
-
229
- say " Documents: #{stats[:documents]}"
230
- say " Ranges: #{stats[:ranges]}"
231
- say " Definitions: #{stats[:definitions]}"
232
- say " References: #{stats[:references]}"
233
- say " Hovers: #{stats[:hovers]}"
234
- puts
235
-
236
- say " Vertex Types:", :cyan
237
- stats[:vertices][:by_type].each do |type, count|
238
- say " #{type}: #{count}"
239
- end
240
- puts
241
-
242
- say " Edge Types:", :cyan
243
- stats[:edges][:by_type].each do |type, count|
244
- say " #{type}: #{count}"
245
- end
246
- end
247
-
248
117
  def handle_error(error, verbose)
249
118
  message = "Error: #{error.message}"
250
119
  message += "\n#{error.backtrace.join("\n")}" if verbose
251
120
  abort message
252
121
  end
253
-
254
- def symbolize_keys(hash)
255
- hash.transform_keys(&:to_sym)
256
- end
257
122
  end
258
123
  end
@@ -24,6 +24,7 @@ module Hind
24
24
 
25
25
  @global_state = GlobalState.new
26
26
  @document_ids = {}
27
+ @current_document_id = nil
27
28
  @lsif_data = []
28
29
  @current_uri = nil
29
30
 
@@ -33,37 +34,49 @@ module Hind
33
34
  def collect_declarations(files)
34
35
  files.each do |path, content|
35
36
  @current_uri = path
36
- ast = Parser.new(content).parse
37
- visitor = DeclarationVisitor.new(self, path)
38
- visitor.visit(ast)
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
39
49
  end
40
50
 
41
- { declarations: @global_state.declarations }
51
+ {declarations: @global_state.declarations}
42
52
  end
43
53
 
44
54
  def process_file(params)
45
- content = params[:content]
46
55
  @current_uri = params[:uri]
56
+ content = params[:content]
57
+
58
+ @document_id = nil
59
+ @current_document_id = nil
47
60
 
48
61
  setup_document
49
62
  ast = Parser.new(content).parse
50
63
 
51
- # Process declarations first to update any missing ones
52
- visitor = DeclarationVisitor.new(self, @current_uri)
53
- visitor.visit(ast)
54
-
55
- # Then process references
56
64
  visitor = ReferenceVisitor.new(self, @current_uri)
57
65
  visitor.visit(ast)
58
66
 
59
- finalize_document
60
- @lsif_data
67
+ result = @lsif_data
68
+ finalize_document_state
69
+ result
61
70
  end
62
71
 
63
72
  def register_declaration(declaration)
64
73
  return unless @current_uri && declaration[:node]
65
74
 
66
75
  qualified_name = declaration[:name]
76
+
77
+ setup_document if @document_id.nil?
78
+ current_doc_id = @document_id
79
+
67
80
  range_id = create_range(declaration[:node].location, declaration[:node].location)
68
81
  return unless range_id
69
82
 
@@ -72,7 +85,8 @@ module Hind
72
85
 
73
86
  def_result_id = emit_vertex('definitionResult')
74
87
  emit_edge('textDocument/definition', result_set_id, def_result_id)
75
- emit_edge('item', def_result_id, [range_id], 'definitions')
88
+
89
+ emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
76
90
 
77
91
  hover_content = generate_hover_content(declaration)
78
92
  hover_id = emit_vertex('hoverResult', {
@@ -88,74 +102,28 @@ module Hind
88
102
  scope: declaration[:scope],
89
103
  file: @current_uri,
90
104
  range_id: range_id,
91
- result_set_id: result_set_id
105
+ result_set_id: result_set_id,
106
+ document_id: current_doc_id
92
107
  }.merge(declaration))
108
+
109
+ result_set_id
93
110
  end
94
111
 
95
112
  def register_reference(reference)
96
113
  return unless @current_uri && reference[:node]
97
114
  return unless @global_state.has_declaration?(reference[:name])
98
115
 
116
+ setup_document if @document_id.nil?
117
+ current_doc_id = @document_id
118
+
99
119
  range_id = create_range(reference[:node].location, reference[:node].location)
100
120
  return unless range_id
101
121
 
102
122
  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])
105
- end
106
-
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
123
+ return unless declaration[:result_set_id]
140
124
 
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
125
+ @global_state.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
126
+ emit_edge('next', range_id, declaration[:result_set_id])
159
127
  end
160
128
 
161
129
  private
@@ -171,10 +139,11 @@ module Hind
171
139
  }
172
140
  })
173
141
 
174
- @global_state.project_id = emit_vertex('project', { kind: 'ruby' })
142
+ @global_state.project_id = emit_vertex('project', {kind: 'ruby'})
175
143
  end
176
144
 
177
145
  def setup_document
146
+ return if @document_id
178
147
  return unless @current_uri
179
148
 
180
149
  file_path = File.join(@metadata[:projectRoot], @current_uri)
@@ -183,17 +152,19 @@ module Hind
183
152
  uri: path_to_uri(file_path),
184
153
  languageId: 'ruby'
185
154
  })
155
+
186
156
  @document_ids[@current_uri] = @document_id
157
+ @current_document_id = @document_id
187
158
 
188
159
  emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
189
160
  end
190
161
 
191
- def finalize_document
192
- return unless @current_uri
162
+ def finalize_document_state
163
+ return unless @current_uri && @document_id
193
164
 
194
165
  ranges = @global_state.get_ranges_for_file(@current_uri)
195
166
  if ranges&.any?
196
- emit_edge('contains', @document_id, ranges)
167
+ emit_edge('contains', @document_id, ranges, nil, @document_id)
197
168
  end
198
169
  end
199
170
 
@@ -235,7 +206,7 @@ module Hind
235
206
  @vertex_id - 1
236
207
  end
237
208
 
238
- def emit_edge(label, out_v, in_v, property = nil)
209
+ def emit_edge(label, out_v, in_v, property = nil, doc_id = nil)
239
210
  return unless out_v && valid_in_v?(in_v)
240
211
 
241
212
  edge = {
@@ -251,8 +222,10 @@ module Hind
251
222
  edge[:inV] = in_v
252
223
  end
253
224
 
254
- edge[:document] = @document_id if label == 'item'
255
- edge[:property] = property if property
225
+ if label == 'item'
226
+ edge[:document] = doc_id || @current_document_id
227
+ edge[:property] = property if property
228
+ end
256
229
 
257
230
  @lsif_data << edge
258
231
  @vertex_id += 1
@@ -261,11 +234,6 @@ module Hind
261
234
 
262
235
  def generate_hover_content(declaration)
263
236
  case declaration[:type]
264
- when :method
265
- sig = []
266
- sig << "def #{declaration[:name]}"
267
- sig << "(#{declaration[:params]})" if declaration[:params]
268
- sig.join
269
237
  when :class
270
238
  hover = ["class #{declaration[:name]}"]
271
239
  hover << " < #{declaration[:superclass]}" if declaration[:superclass]
@@ -273,7 +241,8 @@ module Hind
273
241
  when :module
274
242
  "module #{declaration[:name]}"
275
243
  when :constant
276
- "#{declaration[:name]} = ..."
244
+ value_info = declaration[:node].value ? " = #{declaration[:node].value.inspect}" : ''
245
+ "#{declaration[:name]}#{value_info}"
277
246
  else
278
247
  declaration[:name].to_s
279
248
  end
@@ -301,7 +270,7 @@ module Hind
301
270
 
302
271
  def path_to_uri(path)
303
272
  return nil unless path
304
- normalized_path = path.gsub('\\', '/')
273
+ normalized_path = path.tr('\\', '/')
305
274
  normalized_path = normalized_path.sub(%r{^file://}, '')
306
275
  absolute_path = File.expand_path(normalized_path)
307
276
  "file://#{absolute_path}"
@@ -13,10 +13,6 @@ module Hind
13
13
  @result_sets = {} # {qualified_name => result_set_id}
14
14
  @ranges = {} # {file_path => [range_ids]}
15
15
  @project_id = nil
16
-
17
- # Method visibility tracking
18
- @visibility_stack = [] # Stack of method visibility states per scope
19
- @current_visibility = :public
20
16
  end
21
17
 
22
18
  def add_declaration(qualified_name, data)
@@ -24,12 +20,12 @@ module Hind
24
20
  @result_sets[qualified_name] = data[:result_set_id] if data[:result_set_id]
25
21
  end
26
22
 
27
- def add_reference(qualified_name, file_path, range_id, type = :reference)
23
+ def add_reference(qualified_name, file_path, range_id, document_id)
28
24
  @references[qualified_name] ||= []
29
25
  @references[qualified_name] << {
30
26
  file: file_path,
31
27
  range_id: range_id,
32
- type: type
28
+ document_id: document_id
33
29
  }
34
30
  end
35
31
 
@@ -58,98 +54,31 @@ module Hind
58
54
  @ranges[file_path] || []
59
55
  end
60
56
 
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
57
  def find_constant_declaration(name, current_scope)
115
58
  return name if has_declaration?(name)
116
59
 
117
- # Try with current scope
118
60
  if current_scope && !current_scope.empty?
119
61
  qualified_name = "#{current_scope}::#{name}"
120
62
  return qualified_name if has_declaration?(qualified_name)
121
63
 
122
- # Try parent scopes
123
64
  scope_parts = current_scope.split('::')
124
65
  while scope_parts.any?
125
66
  scope_parts.pop
126
- qualified_name = scope_parts.empty? ? name : "#{scope_parts.join('::')}::#{name}"
67
+ qualified_name = scope_parts.empty? ? name : "#{scope_parts.join("::")}::#{name}"
127
68
  return qualified_name if has_declaration?(qualified_name)
128
69
  end
129
70
  end
130
71
 
131
- # Try top level
132
72
  has_declaration?(name) ? name : nil
133
73
  end
134
74
 
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
75
  def debug_info
146
76
  {
147
77
  declarations_count: @declarations.size,
148
78
  references_count: @references.values.sum(&:size),
149
79
  result_sets_count: @result_sets.size,
150
80
  ranges_count: @ranges.values.sum(&:size),
151
- declaration_types: declaration_types_count,
152
- reference_types: reference_types_count
81
+ declaration_types: declaration_types_count
153
82
  }
154
83
  end
155
84
 
@@ -160,12 +89,6 @@ module Hind
160
89
  counts[decl[:type]] += 1
161
90
  end
162
91
  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
168
- end
169
92
  end
170
93
  end
171
94
  end
@@ -10,16 +10,12 @@ module Hind
10
10
  @generator = generator
11
11
  @file_path = file_path
12
12
  @current_scope = []
13
- @current_visibility = :public
14
- @visibility_stack = []
15
- @in_singleton_class = false
16
13
  end
17
14
 
18
15
  def visit_class_node(node)
19
16
  @current_scope.push(node.constant_path.slice)
20
17
  class_name = current_scope_name
21
18
 
22
- # Register class declaration
23
19
  @generator.register_declaration({
24
20
  type: :class,
25
21
  name: class_name,
@@ -28,10 +24,7 @@ module Hind
28
24
  superclass: node.superclass&.slice
29
25
  })
30
26
 
31
- # Process the class body with proper scope and visibility
32
- push_visibility(:public)
33
27
  super
34
- pop_visibility
35
28
  @current_scope.pop
36
29
  end
37
30
 
@@ -46,32 +39,10 @@ module Hind
46
39
  scope: @current_scope[0..-2].join('::')
47
40
  })
48
41
 
49
- push_visibility(:public)
50
42
  super
51
- pop_visibility
52
43
  @current_scope.pop
53
44
  end
54
45
 
55
- def visit_def_node(node)
56
- method_name = node.name.to_s
57
- # Use '#' for instance methods
58
- qualified_name = @in_singleton_class ?
59
- "#{current_scope_name}.#{method_name}" :
60
- "#{current_scope_name}##{method_name}"
61
-
62
- @generator.register_declaration({
63
- type: :method,
64
- name: qualified_name,
65
- node: node,
66
- scope: current_scope_name,
67
- visibility: current_visibility,
68
- params: node.parameters&.slice,
69
- instance_method: !@in_singleton_class
70
- })
71
-
72
- super
73
- end
74
-
75
46
  def visit_constant_write_node(node)
76
47
  return unless node.name
77
48
 
@@ -88,152 +59,11 @@ module Hind
88
59
  super
89
60
  end
90
61
 
91
- def visit_singleton_class_node(node)
92
- if node.expression.is_a?(Prism::SelfNode)
93
- # class << self
94
- @in_singleton_class = true
95
- super
96
- @in_singleton_class = false
97
- else
98
- # Process regular singleton class
99
- super
100
- end
101
- end
102
-
103
- def visit_call_node(node)
104
- method_name = node.name.to_s
105
- case method_name
106
- when 'private', 'protected', 'public'
107
- handle_visibility_method(node)
108
- when 'attr_reader', 'attr_writer', 'attr_accessor'
109
- handle_attribute_method(node)
110
- end
111
-
112
- super
113
- end
114
-
115
- def visit_class_variable_write_node(node)
116
- return unless node.name
117
-
118
- var_name = node.name.to_s
119
- qualified_name = "#{current_scope_name}::#{var_name}"
120
-
121
- @generator.register_declaration({
122
- type: :class_variable,
123
- name: qualified_name,
124
- node: node,
125
- scope: current_scope_name
126
- })
127
-
128
- super
129
- end
130
-
131
- def visit_instance_variable_write_node(node)
132
- return unless node.name && current_scope_name
133
-
134
- var_name = node.name.to_s
135
- qualified_name = "#{current_scope_name}##{var_name}"
136
-
137
- @generator.register_declaration({
138
- type: :instance_variable,
139
- name: qualified_name,
140
- node: node,
141
- scope: current_scope_name
142
- })
143
-
144
- super
145
- end
146
-
147
62
  private
148
63
 
149
64
  def current_scope_name
150
65
  @current_scope.join('::')
151
66
  end
152
-
153
- def current_visibility
154
- @current_visibility
155
- end
156
-
157
- def push_visibility(visibility)
158
- @visibility_stack.push(@current_visibility)
159
- @current_visibility = visibility
160
- end
161
-
162
- def pop_visibility
163
- @current_visibility = @visibility_stack.pop || :public
164
- end
165
-
166
- def handle_visibility_method(node)
167
- visibility = node.name.to_sym
168
-
169
- if node.arguments&.arguments&.empty?
170
- # Global visibility change
171
- @current_visibility = visibility
172
- elsif node.arguments
173
- # Per-method visibility change
174
- node.arguments.arguments.each do |arg|
175
- next unless arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
176
-
177
- method_name = arg.value.to_s
178
- qualified_name = "#{current_scope_name}##{method_name}"
179
-
180
- if @generator.global_state.has_declaration?(qualified_name)
181
- @generator.global_state.get_declaration(qualified_name)[:visibility] = visibility
182
- end
183
- end
184
- end
185
- end
186
-
187
- def handle_attribute_method(node)
188
- return unless node.arguments
189
-
190
- attr_type = node.name.to_s
191
- node.arguments.arguments.each do |arg|
192
- next unless arg.respond_to?(:value)
193
-
194
- attr_name = arg.value.to_s
195
- register_attribute(attr_name, attr_type)
196
- end
197
- end
198
-
199
- def register_attribute(attr_name, attr_type)
200
- base_name = "#{current_scope_name}##{attr_name}"
201
-
202
- # Register reader method if applicable
203
- if %w[attr_reader attr_accessor].include?(attr_type)
204
- @generator.register_declaration({
205
- type: :method,
206
- name: base_name,
207
- node: nil, # Synthetic node
208
- scope: current_scope_name,
209
- visibility: current_visibility,
210
- synthetic: true,
211
- kind: :reader
212
- })
213
- end
214
-
215
- # Register writer method if applicable
216
- if %w[attr_writer attr_accessor].include?(attr_type)
217
- @generator.register_declaration({
218
- type: :method,
219
- name: "#{base_name}=",
220
- node: nil, # Synthetic node
221
- scope: current_scope_name,
222
- visibility: current_visibility,
223
- synthetic: true,
224
- kind: :writer
225
- })
226
- end
227
-
228
- # Register the instance variable
229
- @generator.register_declaration({
230
- type: :instance_variable,
231
- name: "@#{attr_name}",
232
- node: nil, # Synthetic node
233
- scope: current_scope_name,
234
- synthetic: true
235
- })
236
- end
237
67
  end
238
68
  end
239
69
  end
@@ -1,4 +1,3 @@
1
- # lib/hind/lsif/visitors/reference_visitor.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module Hind
@@ -12,27 +11,6 @@ module Hind
12
11
  @current_scope = []
13
12
  end
14
13
 
15
- # Method calls
16
- def visit_call_node(node)
17
- return unless node.name && node.location
18
-
19
- method_name = node.name.to_s
20
- qualified_names = generate_qualified_names_for_call(node)
21
-
22
- qualified_names.each do |qualified_name|
23
- @generator.register_reference({
24
- type: :method,
25
- name: qualified_name,
26
- node: node,
27
- scope: current_scope_name,
28
- call_type: :instance_method
29
- })
30
- end
31
-
32
- super
33
- end
34
-
35
- # Class/module references
36
14
  def visit_constant_read_node(node)
37
15
  return unless node.name
38
16
 
@@ -49,7 +27,6 @@ module Hind
49
27
  super
50
28
  end
51
29
 
52
- # Constant path references (e.g., A::B::C)
53
30
  def visit_constant_path_node(node)
54
31
  qualified_name = node.slice
55
32
 
@@ -63,87 +40,6 @@ module Hind
63
40
  super
64
41
  end
65
42
 
66
- # Instance variable references
67
- def visit_instance_variable_read_node(node)
68
- return unless node.name && current_scope_name
69
-
70
- var_name = node.name.to_s
71
- qualified_name = "#{current_scope_name}##{var_name}"
72
-
73
- @generator.register_reference({
74
- type: :instance_variable,
75
- name: qualified_name,
76
- node: node,
77
- scope: current_scope_name
78
- })
79
-
80
- super
81
- end
82
-
83
- # Class variable references
84
- def visit_class_variable_read_node(node)
85
- return unless node.name && current_scope_name
86
-
87
- var_name = node.name.to_s
88
- qualified_name = "#{current_scope_name}::#{var_name}"
89
-
90
- @generator.register_reference({
91
- type: :class_variable,
92
- name: qualified_name,
93
- node: node,
94
- scope: current_scope_name
95
- })
96
-
97
- super
98
- end
99
-
100
- # Singleton method calls (class methods)
101
- def visit_constant_path_call_node(node)
102
- return unless node.name
103
-
104
- method_name = node.name.to_s
105
- receiver_name = node.receiver.slice
106
- qualified_name = "#{receiver_name}.#{method_name}"
107
-
108
- @generator.register_reference({
109
- type: :method,
110
- name: qualified_name,
111
- node: node,
112
- scope: current_scope_name,
113
- call_type: :class_method
114
- })
115
-
116
- super
117
- end
118
-
119
- # Super method calls
120
- def visit_super_node(node)
121
- return unless current_scope_name
122
-
123
- # Extract current method name from scope
124
- current_method = current_method_name
125
- return unless current_method
126
-
127
- # Try to find the superclass method
128
- if in_class_scope?
129
- superclass = find_superclass
130
- if superclass
131
- qualified_name = "#{superclass}##{current_method}"
132
-
133
- @generator.register_reference({
134
- type: :method,
135
- name: qualified_name,
136
- node: node,
137
- scope: current_scope_name,
138
- call_type: :super
139
- })
140
- end
141
- end
142
-
143
- super
144
- end
145
-
146
- # Track class/module scope
147
43
  def visit_class_node(node)
148
44
  @current_scope.push(node.constant_path.slice)
149
45
  super
@@ -161,61 +57,6 @@ module Hind
161
57
  def current_scope_name
162
58
  @current_scope.join('::')
163
59
  end
164
-
165
- def generate_qualified_names_for_call(node)
166
- qualified_names = []
167
- method_name = node.name.to_s
168
-
169
- # Try with current scope first
170
- qualified_names << "#{current_scope_name}##{method_name}" unless current_scope_name.empty?
171
-
172
- # Try with receiver's type if available
173
- if node.receiver
174
- case node.receiver
175
- when Prism::ConstantReadNode
176
- qualified_names << "#{node.receiver.name}##{method_name}"
177
- when Prism::ConstantPathNode
178
- qualified_names << "#{node.receiver.slice}##{method_name}"
179
- when Prism::CallNode
180
- # Method chaining - try both instance and class methods
181
- if node.receiver.name
182
- qualified_names << "#{node.receiver.name}##{method_name}"
183
- qualified_names << "#{node.receiver.name}.#{method_name}"
184
- end
185
- when Prism::InstanceVariableReadNode
186
- # Instance variable calls - try current class context
187
- qualified_names << "#{current_scope_name}##{method_name}" if current_scope_name
188
- end
189
- end
190
-
191
- # Try as a standalone method
192
- qualified_names << method_name
193
-
194
- # Add potential class method variant
195
- qualified_names << "#{current_scope_name}.#{method_name}" unless current_scope_name.empty?
196
-
197
- qualified_names.uniq
198
- end
199
-
200
- def current_method_name
201
- # Try to find the nearest method node in the AST
202
- # This is a simplified version - you might need to enhance this
203
- # based on your specific needs
204
- "current_method"
205
- end
206
-
207
- def in_class_scope?
208
- # Check if we're currently in a class definition
209
- !@current_scope.empty? && @generator.global_state.declarations[@current_scope.last]&.[](:type) == :class
210
- end
211
-
212
- def find_superclass
213
- return unless in_class_scope?
214
-
215
- current_class = @current_scope.last
216
- class_declaration = @generator.global_state.declarations[current_class]
217
- class_declaration&.[](:superclass)
218
- end
219
60
  end
220
61
  end
221
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.6'
4
+ VERSION = '0.1.7'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hind
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aboobacker MK
@@ -111,7 +111,6 @@ 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
115
114
  - lib/hind/lsif/visitors/declaration_visitor.rb
116
115
  - lib/hind/lsif/visitors/reference_visitor.rb
117
116
  - lib/hind/parser.rb
@@ -1,255 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hind
4
- module LSIF
5
- class Visitor < Prism::Visitor
6
- def initialize(generator)
7
- @generator = generator
8
- @current_scope = []
9
- end
10
-
11
- def visit_def_node(node)
12
- method_name = node.name.to_s
13
- qualified_name = current_scope_name.empty? ? method_name : "#{current_scope_name}##{method_name}"
14
-
15
- range_id = @generator.create_range(node.location, node.location)
16
- result_set_id = @generator.emit_vertex('resultSet')
17
- @generator.emit_edge('next', range_id, result_set_id)
18
-
19
- def_result_id = @generator.emit_vertex('definitionResult')
20
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
21
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
22
-
23
- # Generate method signature for hover
24
- sig = []
25
- sig << "def #{qualified_name}"
26
- sig << "(#{node.parameters.slice})" if node.parameters
27
-
28
- hover_id = @generator.emit_vertex('hoverResult', {
29
- contents: [{
30
- language: 'ruby',
31
- value: sig.join
32
- }]
33
- })
34
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
35
-
36
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
37
-
38
- super
39
- end
40
-
41
- def visit_class_node(node)
42
- @current_scope.push(node.constant_path.slice)
43
- class_name = current_scope_name
44
-
45
- range_id = @generator.create_range(node.location, node.location)
46
- result_set_id = @generator.emit_vertex('resultSet')
47
- @generator.emit_edge('next', range_id, result_set_id)
48
-
49
- def_result_id = @generator.emit_vertex('definitionResult')
50
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
51
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
52
-
53
- # Generate hover with inheritance info
54
- hover = []
55
- class_def = "class #{class_name}"
56
- class_def += " < #{node.superclass.slice}" if node.superclass
57
-
58
- hover << if node.superclass
59
- "#{class_def}\n\nInherits from: #{node.superclass.slice}"
60
- else
61
- class_def
62
- end
63
-
64
- hover_id = @generator.emit_vertex('hoverResult', {
65
- contents: [{
66
- language: 'ruby',
67
- value: hover.join("\n")
68
- }]
69
- })
70
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
71
-
72
- @generator.add_to_global_state(class_name, result_set_id, range_id)
73
-
74
- # Handle inheritance
75
- visit_inheritance(node.superclass) if node.superclass
76
-
77
- super
78
- @current_scope.pop
79
- end
80
-
81
- def visit_module_node(node)
82
- @current_scope.push(node.constant_path.slice)
83
- module_name = current_scope_name
84
-
85
- range_id = @generator.create_range(node.location, node.location)
86
- result_set_id = @generator.emit_vertex('resultSet')
87
- @generator.emit_edge('next', range_id, result_set_id)
88
-
89
- def_result_id = @generator.emit_vertex('definitionResult')
90
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
91
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
92
-
93
- hover_id = @generator.emit_vertex('hoverResult', {
94
- contents: [{
95
- language: 'ruby',
96
- value: "module #{module_name}"
97
- }]
98
- })
99
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
100
-
101
- @generator.add_to_global_state(module_name, result_set_id, range_id)
102
-
103
- super
104
- @current_scope.pop
105
- end
106
-
107
- def visit_call_node(node)
108
- return unless node.name && node.location
109
-
110
- method_name = node.name.to_s
111
- qualified_names = []
112
-
113
- # Try with current scope first
114
- qualified_names << "#{current_scope_name}##{method_name}" unless current_scope_name.empty?
115
-
116
- # Try with receiver's type if available
117
- if node.receiver
118
- case node.receiver.type
119
- when :constant_read
120
- qualified_names << "#{node.receiver.name}##{method_name}"
121
- when :call
122
- # Handle method chaining
123
- qualified_names << "#{node.receiver.name}##{method_name}" if node.receiver.name
124
- when :instance_variable_read
125
- # Handle instance variable calls
126
- qualified_names << "#{current_scope_name}##{method_name}" if current_scope_name
127
- end
128
- end
129
-
130
- # Try as a standalone method
131
- qualified_names << method_name
132
-
133
- # Add references for matching qualified names
134
- qualified_names.each do |qualified_name|
135
- next unless @generator.global_state.result_sets[qualified_name]
136
-
137
- range_id = @generator.create_range(node.location, node.location)
138
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
139
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
140
- break # Stop after finding first match
141
- end
142
-
143
- super
144
- end
145
-
146
- def visit_constant_read_node(node)
147
- return unless node.name
148
-
149
- constant_name = node.name.to_s
150
- qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
151
-
152
- return 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
-
158
- super
159
- end
160
-
161
- def visit_constant_write_node(node)
162
- return unless node.name
163
-
164
- constant_name = node.name.to_s
165
- qualified_name = @current_scope.empty? ? constant_name : "#{current_scope_name}::#{constant_name}"
166
-
167
- range_id = @generator.create_range(node.location, node.location)
168
- result_set_id = @generator.emit_vertex('resultSet')
169
- @generator.emit_edge('next', range_id, result_set_id)
170
-
171
- def_result_id = @generator.emit_vertex('definitionResult')
172
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
173
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
174
-
175
- hover_id = @generator.emit_vertex('hoverResult', {
176
- contents: [{
177
- language: 'ruby',
178
- value: "#{qualified_name} = ..."
179
- }]
180
- })
181
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
182
-
183
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
184
-
185
- super
186
- end
187
-
188
- def visit_instance_variable_read_node(node)
189
- return unless node.name && current_scope_name
190
-
191
- var_name = node.name.to_s
192
- qualified_name = "#{current_scope_name}##{var_name}"
193
-
194
- return unless @generator.global_state.result_sets[qualified_name]
195
-
196
- range_id = @generator.create_range(node.location, node.location)
197
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
198
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
199
-
200
- super
201
- end
202
-
203
- def visit_instance_variable_write_node(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
- range_id = @generator.create_range(node.location, node.location)
210
- result_set_id = @generator.emit_vertex('resultSet')
211
- @generator.emit_edge('next', range_id, result_set_id)
212
-
213
- def_result_id = @generator.emit_vertex('definitionResult')
214
- @generator.emit_edge('textDocument/definition', result_set_id, def_result_id)
215
- @generator.emit_edge('item', def_result_id, [range_id], 'definitions')
216
-
217
- hover_id = @generator.emit_vertex('hoverResult', {
218
- contents: [{
219
- language: 'ruby',
220
- value: "Instance variable #{var_name} in #{current_scope_name}"
221
- }]
222
- })
223
- @generator.emit_edge('textDocument/hover', result_set_id, hover_id)
224
-
225
- @generator.add_to_global_state(qualified_name, result_set_id, range_id)
226
-
227
- super
228
- end
229
-
230
- private
231
-
232
- def current_scope_name
233
- @current_scope.join('::')
234
- end
235
-
236
- def visit_inheritance(node)
237
- case node.type
238
- when :constant_read_node, :constant_path_node
239
- range_id = @generator.create_range(node.location, node.location)
240
- qualified_name = case node.type
241
- when :constant_read_node
242
- node.name.to_s
243
- when :constant_path_node
244
- node.slice
245
- end
246
-
247
- return unless @generator.global_state.result_sets[qualified_name]
248
-
249
- @generator.global_state.add_reference(qualified_name, @generator.metadata[:uri], range_id)
250
- @generator.emit_edge('next', range_id, @generator.global_state.result_sets[qualified_name])
251
- end
252
- end
253
- end
254
- end
255
- end