hind 0.1.6 → 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: 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