hind 0.1.5 → 0.1.6
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 +4 -4
- data/exe/hind +2 -2
- data/lib/hind/cli.rb +189 -68
- data/lib/hind/lsif/generator.rb +219 -91
- data/lib/hind/lsif/global_state.rb +150 -11
- data/lib/hind/lsif/visitor.rb +28 -41
- data/lib/hind/lsif/visitors/declaration_visitor.rb +239 -0
- data/lib/hind/lsif/visitors/reference_visitor.rb +221 -0
- data/lib/hind/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66f58f99f053b938815d051ff6404a478a4f340178f74556ce6ab63f0a93c2e4
|
4
|
+
data.tar.gz: 02e04a6c88687f9baf37013fc216dc7df52b5fa6e81f60170cbb31146335c91b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 529d648a5f0677180c4c73daa8d316661f4b82b7bd4802318c8bb42a7ed7bd8d2ffb38b69a82a274483638ea7bc1c161ea9d6bb6f3c7f84e3ff75464a1968c75
|
7
|
+
data.tar.gz: dc734f7623cffa1c0cc6260627d14916e6e9809c3b06afcad1bd87015c8c32b8bbe979ed978abb922adc3af7a19c7c3099f3217c00016711260aacd5079fab6c
|
data/exe/hind
CHANGED
data/lib/hind/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require 'thor'
|
|
4
4
|
require 'json'
|
5
5
|
require 'pathname'
|
6
6
|
require 'fileutils'
|
7
|
+
require 'yaml'
|
7
8
|
|
8
9
|
module Hind
|
9
10
|
class CLI < Thor
|
@@ -16,43 +17,47 @@ module Hind
|
|
16
17
|
method_option :glob, type: :string, aliases: '-g', default: '**/*.rb', desc: 'File pattern to match'
|
17
18
|
method_option :force, type: :boolean, aliases: '-f', desc: 'Overwrite output file if it exists'
|
18
19
|
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'
|
19
21
|
def lsif
|
20
|
-
|
21
|
-
|
22
|
+
config = load_config(options[:config])
|
23
|
+
opts = config.merge(symbolize_keys(options))
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
+
validate_directory(opts[:directory])
|
26
|
+
validate_output_file(opts[:output], opts[:force])
|
25
27
|
|
26
|
-
|
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]
|
27
32
|
|
28
33
|
begin
|
29
|
-
generate_lsif(files,
|
30
|
-
say "\nLSIF data has been written to: #{
|
31
|
-
rescue => e
|
32
|
-
|
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])
|
33
38
|
end
|
34
39
|
end
|
35
40
|
|
36
|
-
desc '
|
37
|
-
method_option :
|
38
|
-
method_option :
|
39
|
-
method_option :
|
40
|
-
|
41
|
-
|
42
|
-
def scip
|
43
|
-
validate_directory(options[:directory])
|
44
|
-
validate_output_file(options[:output], options[:force])
|
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])
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
+
begin
|
49
|
+
checker = Hind::LSIF::Checker.new(options[:file])
|
50
|
+
results = checker.check
|
48
51
|
|
49
|
-
|
52
|
+
if options[:json]
|
53
|
+
puts JSON.pretty_generate(results)
|
54
|
+
else
|
55
|
+
print_check_results(results)
|
56
|
+
end
|
50
57
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
rescue => e
|
55
|
-
abort "Error generating SCIP: #{e.message}"
|
58
|
+
exit(1) if !results[:valid] || (options[:strict] && results[:warnings].any?)
|
59
|
+
rescue StandardError => e
|
60
|
+
handle_error(e, options[:verbose])
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
@@ -61,14 +66,81 @@ module Hind
|
|
61
66
|
say "Hind version #{Hind::VERSION}"
|
62
67
|
end
|
63
68
|
|
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
|
+
|
64
81
|
private
|
65
82
|
|
83
|
+
def generate_lsif(files, options)
|
84
|
+
# Initialize generator with absolute project root
|
85
|
+
generator = Hind::LSIF::Generator.new(
|
86
|
+
{
|
87
|
+
vertex_id: 1,
|
88
|
+
initial: true,
|
89
|
+
projectRoot: File.expand_path(options[:directory])
|
90
|
+
}
|
91
|
+
)
|
92
|
+
|
93
|
+
# Create file content map with relative paths
|
94
|
+
file_contents = {}
|
95
|
+
files.each do |file|
|
96
|
+
absolute_path = File.expand_path(file)
|
97
|
+
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
|
104
|
+
end
|
105
|
+
|
106
|
+
File.open(options[:output], 'w') do |output_file|
|
107
|
+
say "First pass: Collecting declarations...", :cyan if options[:verbose]
|
108
|
+
|
109
|
+
# First pass: Process all files to collect declarations
|
110
|
+
declaration_data = generator.collect_declarations(file_contents)
|
111
|
+
|
112
|
+
say "Found #{declaration_data[:declarations].size} declarations", :cyan if options[:verbose]
|
113
|
+
say "Processing files...", :cyan if options[:verbose]
|
114
|
+
|
115
|
+
# Second pass: Process each file
|
116
|
+
file_contents.each do |relative_path, content|
|
117
|
+
if options[:verbose]
|
118
|
+
say "Processing file: #{relative_path}", :cyan
|
119
|
+
end
|
120
|
+
|
121
|
+
lsif_data = generator.process_file(
|
122
|
+
content: content,
|
123
|
+
uri: relative_path
|
124
|
+
)
|
125
|
+
|
126
|
+
output_file.puts(lsif_data.map(&:to_json).join("\n"))
|
127
|
+
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
|
+
end
|
134
|
+
end
|
135
|
+
|
66
136
|
def validate_directory(directory)
|
67
137
|
abort "Error: Directory '#{directory}' does not exist" unless Dir.exist?(directory)
|
68
138
|
end
|
69
139
|
|
70
140
|
def validate_output_file(output, force)
|
71
|
-
|
141
|
+
if File.exist?(output) && !force
|
142
|
+
abort "Error: Output file '#{output}' already exists. Use --force to overwrite."
|
143
|
+
end
|
72
144
|
|
73
145
|
# Ensure output directory exists
|
74
146
|
FileUtils.mkdir_p(File.dirname(output))
|
@@ -78,60 +150,109 @@ module Hind
|
|
78
150
|
pattern = File.join(directory, glob)
|
79
151
|
files = Dir.glob(pattern)
|
80
152
|
|
81
|
-
exclude_patterns
|
82
|
-
|
153
|
+
if exclude_patterns
|
154
|
+
exclude_patterns.each do |exclude|
|
155
|
+
files.reject! { |f| File.fnmatch?(exclude, f) }
|
156
|
+
end
|
83
157
|
end
|
84
158
|
|
85
159
|
files
|
86
160
|
end
|
87
161
|
|
88
|
-
def
|
89
|
-
|
90
|
-
vertex_id = 1
|
91
|
-
initial = true
|
162
|
+
def load_config(config_path)
|
163
|
+
return {} unless config_path && File.exist?(config_path)
|
92
164
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
relative_path = Pathname.new(file).relative_path_from(Pathname.new(options[:directory])).to_s
|
98
|
-
|
99
|
-
begin
|
100
|
-
generator = Hind::LSIF::Generator.new(
|
101
|
-
{
|
102
|
-
uri: relative_path,
|
103
|
-
vertex_id: vertex_id,
|
104
|
-
initial: initial,
|
105
|
-
projectRoot: options[:directory]
|
106
|
-
},
|
107
|
-
global_state
|
108
|
-
)
|
109
|
-
|
110
|
-
output = generator.generate(File.read(file))
|
111
|
-
vertex_id = output.last[:id].to_i + 1
|
112
|
-
output_file.puts(output.map(&:to_json).join("\n"))
|
113
|
-
initial = false
|
114
|
-
rescue => e
|
115
|
-
warn "Warning: Failed to process file '#{file}': #{e.message}"
|
116
|
-
next
|
117
|
-
end
|
118
|
-
end
|
165
|
+
begin
|
166
|
+
YAML.load_file(config_path) || {}
|
167
|
+
rescue StandardError => e
|
168
|
+
abort "Error loading config file: #{e.message}"
|
119
169
|
end
|
120
170
|
end
|
121
171
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
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])
|
125
193
|
end
|
126
194
|
|
127
|
-
def
|
128
|
-
|
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
|
129
200
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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}"
|
134
245
|
end
|
135
246
|
end
|
247
|
+
|
248
|
+
def handle_error(error, verbose)
|
249
|
+
message = "Error: #{error.message}"
|
250
|
+
message += "\n#{error.backtrace.join("\n")}" if verbose
|
251
|
+
abort message
|
252
|
+
end
|
253
|
+
|
254
|
+
def symbolize_keys(hash)
|
255
|
+
hash.transform_keys(&:to_sym)
|
256
|
+
end
|
136
257
|
end
|
137
258
|
end
|