hind 0.1.6 → 0.1.8

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: debf085e96a2d5683f8aac3870ab5c06ce0b28adc8dfa960227fb0a763defcee
4
+ data.tar.gz: 475b21bdbc4502c445a24be086e131c511f53972f113e7dda6e2b52eb03b16b1
5
5
  SHA512:
6
- metadata.gz: 529d648a5f0677180c4c73daa8d316661f4b82b7bd4802318c8bb42a7ed7bd8d2ffb38b69a82a274483638ea7bc1c161ea9d6bb6f3c7f84e3ff75464a1968c75
7
- data.tar.gz: dc734f7623cffa1c0cc6260627d14916e6e9809c3b06afcad1bd87015c8c32b8bbe979ed978abb922adc3af7a19c7c3099f3217c00016711260aacd5079fab6c
6
+ metadata.gz: da48ed207a58c99a9a544c2f5f2d0b3c88aecd7fa36ef7fc348064f925033ae33b7702c04d1147970a583b27a2031c5de7ca89b4b9d0aefb731ae0b3ffa93ce7
7
+ data.tar.gz: 88f86b97957a68ab50d153311d6d07a0ab0d99f39dfabe2409039ef86fc22af6f162419ddb926de58c938ebc9de165291c30e33556ac25db0d467ec1c450e610
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]
25
+ say "Found #{files.length} files to process", :green if options[:verbose]
32
26
 
33
27
  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])
47
-
48
- 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,41 +54,56 @@ 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]
70
+
71
+ # Write initial LSIF data (metadata and project vertices)
72
+ initial_data = generator.get_initial_data
73
+ if initial_data&.any?
74
+ say 'Writing initial LSIF data...', :cyan if options[:verbose]
75
+ output_file.puts(initial_data.map(&:to_json).join("\n"))
76
+ end
108
77
 
109
78
  # First pass: Process all files to collect declarations
110
79
  declaration_data = generator.collect_declarations(file_contents)
111
80
 
112
- say "Found #{declaration_data[:declarations].size} declarations", :cyan if options[:verbose]
113
- say "Processing files...", :cyan if options[:verbose]
81
+ say "Found #{declaration_data[:declarations].size} declarations (classes, modules, constants)", :cyan if options[:verbose]
82
+
83
+ # Write declaration LSIF data next
84
+ if declaration_data[:lsif_data]&.any?
85
+ output_file.puts(declaration_data[:lsif_data].map(&:to_json).join("\n"))
86
+ end
87
+
88
+ say 'Processing files for references...', :cyan if options[:verbose]
114
89
 
115
- # Second pass: Process each file
90
+ # Second pass: Process each file for references
116
91
  file_contents.each do |relative_path, content|
117
92
  if options[:verbose]
118
93
  say "Processing file: #{relative_path}", :cyan
119
94
  end
120
95
 
121
- lsif_data = generator.process_file(
96
+ reference_lsif_data = generator.process_file(
122
97
  content: content,
123
98
  uri: relative_path
124
99
  )
125
-
126
- output_file.puts(lsif_data.map(&:to_json).join("\n"))
100
+ output_file.puts(reference_lsif_data.map(&:to_json).join("\n"))
127
101
  end
128
102
 
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?
103
+ # Finalize and write cross-file references
104
+ say 'Processing cross-file references...', :cyan if options[:verbose]
105
+ final_references = generator.finalize_references
106
+ output_file.puts(final_references.map(&:to_json).join("\n"))
133
107
  end
134
108
  end
135
109
 
@@ -150,109 +124,17 @@ module Hind
150
124
  pattern = File.join(directory, glob)
151
125
  files = Dir.glob(pattern)
152
126
 
153
- if exclude_patterns
154
- exclude_patterns.each do |exclude|
155
- files.reject! { |f| File.fnmatch?(exclude, f) }
156
- end
127
+ exclude_patterns&.each do |exclude|
128
+ files.reject! { |f| File.fnmatch?(exclude, f) }
157
129
  end
158
130
 
159
131
  files
160
132
  end
161
133
 
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
134
  def handle_error(error, verbose)
249
135
  message = "Error: #{error.message}"
250
136
  message += "\n#{error.backtrace.join("\n")}" if verbose
251
137
  abort message
252
138
  end
253
-
254
- def symbolize_keys(hash)
255
- hash.transform_keys(&:to_sym)
256
- end
257
139
  end
258
140
  end
@@ -24,8 +24,10 @@ 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
30
+ @last_vertex_id = @vertex_id
29
31
 
30
32
  initialize_project if metadata[:initial]
31
33
  end
@@ -33,30 +35,82 @@ module Hind
33
35
  def collect_declarations(files)
34
36
  files.each do |path, content|
35
37
  @current_uri = path
36
- ast = Parser.new(content).parse
37
- visitor = DeclarationVisitor.new(self, path)
38
- visitor.visit(ast)
38
+ @document_id = nil
39
+ @current_document_id = nil
40
+
41
+ begin
42
+ ast = Parser.new(content).parse
43
+ setup_document
44
+ visitor = DeclarationVisitor.new(self, path)
45
+ visitor.visit(ast)
46
+ finalize_document_state
47
+ rescue => e
48
+ warn "Warning: Failed to collect declarations from '#{path}': #{e.message}"
49
+ end
39
50
  end
40
51
 
41
- { declarations: @global_state.declarations }
52
+ # Store the last used vertex ID and reset reference index
53
+ @last_vertex_id = @vertex_id
54
+ @last_reference_index = @lsif_data.length
55
+
56
+ {
57
+ declarations: @global_state.declarations,
58
+ lsif_data: @lsif_data
59
+ }
42
60
  end
43
61
 
44
62
  def process_file(params)
45
- content = params[:content]
46
63
  @current_uri = params[:uri]
64
+ content = params[:content]
65
+
66
+ # Restore vertex ID from last declaration pass
67
+ @vertex_id = @last_vertex_id
68
+
69
+ @document_id = nil
70
+ @current_document_id = nil
47
71
 
48
72
  setup_document
49
73
  ast = Parser.new(content).parse
50
74
 
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
75
  visitor = ReferenceVisitor.new(self, @current_uri)
57
76
  visitor.visit(ast)
58
77
 
59
- finalize_document
78
+ finalize_document_state
79
+
80
+ # Update last vertex ID
81
+ @last_vertex_id = @vertex_id
82
+
83
+ # Return only the new LSIF data since last call
84
+ result = @lsif_data[@last_reference_index..]
85
+ @last_reference_index = @lsif_data.length
86
+ result
87
+ end
88
+
89
+ def get_initial_data
90
+ @initial_data
91
+ end
92
+
93
+ def finalize_references
94
+ # Restore vertex ID
95
+ @vertex_id = @last_vertex_id
96
+ # Process all references to create definition results
97
+ @global_state.references.each do |qualified_name, references|
98
+ declaration = @global_state.declarations[qualified_name]
99
+ next unless declaration && declaration[:result_set_id]
100
+
101
+ # Create reference result for this symbol
102
+ ref_result_id = emit_vertex('referenceResult')
103
+ emit_edge('textDocument/references', declaration[:result_set_id], ref_result_id)
104
+
105
+ # Group references by document
106
+ references_by_doc = references.group_by { |ref| ref[:document_id] }
107
+
108
+ # Create item edges for each document's references
109
+ references_by_doc.each do |doc_id, refs|
110
+ emit_edge('item', ref_result_id, refs.map { |r| r[:range_id] }, 'references', doc_id)
111
+ end
112
+ end
113
+
60
114
  @lsif_data
61
115
  end
62
116
 
@@ -64,6 +118,10 @@ module Hind
64
118
  return unless @current_uri && declaration[:node]
65
119
 
66
120
  qualified_name = declaration[:name]
121
+
122
+ setup_document if @document_id.nil?
123
+ current_doc_id = @document_id
124
+
67
125
  range_id = create_range(declaration[:node].location, declaration[:node].location)
68
126
  return unless range_id
69
127
 
@@ -72,7 +130,8 @@ module Hind
72
130
 
73
131
  def_result_id = emit_vertex('definitionResult')
74
132
  emit_edge('textDocument/definition', result_set_id, def_result_id)
75
- emit_edge('item', def_result_id, [range_id], 'definitions')
133
+
134
+ emit_edge('item', def_result_id, [range_id], 'definitions', current_doc_id)
76
135
 
77
136
  hover_content = generate_hover_content(declaration)
78
137
  hover_id = emit_vertex('hoverResult', {
@@ -88,74 +147,28 @@ module Hind
88
147
  scope: declaration[:scope],
89
148
  file: @current_uri,
90
149
  range_id: range_id,
91
- result_set_id: result_set_id
150
+ result_set_id: result_set_id,
151
+ document_id: current_doc_id
92
152
  }.merge(declaration))
153
+
154
+ result_set_id
93
155
  end
94
156
 
95
157
  def register_reference(reference)
96
158
  return unless @current_uri && reference[:node]
97
159
  return unless @global_state.has_declaration?(reference[:name])
98
160
 
161
+ setup_document if @document_id.nil?
162
+ current_doc_id = @document_id
163
+
99
164
  range_id = create_range(reference[:node].location, reference[:node].location)
100
165
  return unless range_id
101
166
 
102
167
  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
168
+ return unless declaration[:result_set_id]
106
169
 
107
- def finalize_cross_references
108
- cross_ref_data = []
109
-
110
- @global_state.references.each do |qualified_name, references|
111
- declaration = @global_state.declarations[qualified_name]
112
- next unless declaration
113
-
114
- result_set_id = declaration[:result_set_id]
115
- next unless result_set_id
116
-
117
- ref_result_id = emit_vertex('referenceResult')
118
- emit_edge('textDocument/references', result_set_id, ref_result_id)
119
-
120
- # Collect all reference range IDs
121
- all_refs = references.map { |ref| ref[:range_id] }
122
- all_refs << declaration[:range_id] if declaration[:range_id]
123
-
124
- # Group references by document
125
- references.group_by { |ref| ref[:file] }.each do |file_path, file_refs|
126
- document_id = @document_ids[file_path]
127
- next unless document_id
128
-
129
- cross_ref_data << {
130
- id: @vertex_id,
131
- type: 'edge',
132
- label: 'item',
133
- outV: ref_result_id,
134
- inVs: all_refs,
135
- document: document_id,
136
- property: 'references'
137
- }
138
- @vertex_id += 1
139
- end
140
-
141
- # Handle document containing the definition
142
- def_file = declaration[:file]
143
- def_document_id = @document_ids[def_file]
144
- if def_document_id && references.none? { |ref| ref[:file] == def_file }
145
- cross_ref_data << {
146
- id: @vertex_id,
147
- type: 'edge',
148
- label: 'item',
149
- outV: ref_result_id,
150
- inVs: all_refs,
151
- document: def_document_id,
152
- property: 'references'
153
- }
154
- @vertex_id += 1
155
- end
156
- end
157
-
158
- cross_ref_data
170
+ @global_state.add_reference(reference[:name], @current_uri, range_id, current_doc_id)
171
+ emit_edge('next', range_id, declaration[:result_set_id])
159
172
  end
160
173
 
161
174
  private
@@ -171,10 +184,11 @@ module Hind
171
184
  }
172
185
  })
173
186
 
174
- @global_state.project_id = emit_vertex('project', { kind: 'ruby' })
187
+ @global_state.project_id = emit_vertex('project', {kind: 'ruby'})
175
188
  end
176
189
 
177
190
  def setup_document
191
+ return if @document_id
178
192
  return unless @current_uri
179
193
 
180
194
  file_path = File.join(@metadata[:projectRoot], @current_uri)
@@ -183,17 +197,19 @@ module Hind
183
197
  uri: path_to_uri(file_path),
184
198
  languageId: 'ruby'
185
199
  })
200
+
186
201
  @document_ids[@current_uri] = @document_id
202
+ @current_document_id = @document_id
187
203
 
188
204
  emit_edge('contains', @global_state.project_id, [@document_id]) if @global_state.project_id
189
205
  end
190
206
 
191
- def finalize_document
192
- return unless @current_uri
207
+ def finalize_document_state
208
+ return unless @current_uri && @document_id
193
209
 
194
210
  ranges = @global_state.get_ranges_for_file(@current_uri)
195
211
  if ranges&.any?
196
- emit_edge('contains', @document_id, ranges)
212
+ emit_edge('contains', @document_id, ranges, nil, @document_id)
197
213
  end
198
214
  end
199
215
 
@@ -235,7 +251,7 @@ module Hind
235
251
  @vertex_id - 1
236
252
  end
237
253
 
238
- def emit_edge(label, out_v, in_v, property = nil)
254
+ def emit_edge(label, out_v, in_v, property = nil, doc_id = nil)
239
255
  return unless out_v && valid_in_v?(in_v)
240
256
 
241
257
  edge = {
@@ -251,8 +267,10 @@ module Hind
251
267
  edge[:inV] = in_v
252
268
  end
253
269
 
254
- edge[:document] = @document_id if label == 'item'
255
- edge[:property] = property if property
270
+ if label == 'item'
271
+ edge[:document] = doc_id || @current_document_id
272
+ edge[:property] = property if property
273
+ end
256
274
 
257
275
  @lsif_data << edge
258
276
  @vertex_id += 1
@@ -261,11 +279,6 @@ module Hind
261
279
 
262
280
  def generate_hover_content(declaration)
263
281
  case declaration[:type]
264
- when :method
265
- sig = []
266
- sig << "def #{declaration[:name]}"
267
- sig << "(#{declaration[:params]})" if declaration[:params]
268
- sig.join
269
282
  when :class
270
283
  hover = ["class #{declaration[:name]}"]
271
284
  hover << " < #{declaration[:superclass]}" if declaration[:superclass]
@@ -273,7 +286,8 @@ module Hind
273
286
  when :module
274
287
  "module #{declaration[:name]}"
275
288
  when :constant
276
- "#{declaration[:name]} = ..."
289
+ value_info = declaration[:node].value ? " = #{declaration[:node].value.inspect}" : ''
290
+ "#{declaration[:name]}#{value_info}"
277
291
  else
278
292
  declaration[:name].to_s
279
293
  end
@@ -301,7 +315,7 @@ module Hind
301
315
 
302
316
  def path_to_uri(path)
303
317
  return nil unless path
304
- normalized_path = path.gsub('\\', '/')
318
+ normalized_path = path.tr('\\', '/')
305
319
  normalized_path = normalized_path.sub(%r{^file://}, '')
306
320
  absolute_path = File.expand_path(normalized_path)
307
321
  "file://#{absolute_path}"