hind 0.1.6 → 0.1.8

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: 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}"