steep 1.10.0 → 2.0.0

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/CLAUDE.md +114 -0
  4. data/README.md +1 -1
  5. data/Rakefile +15 -3
  6. data/Steepfile +13 -13
  7. data/lib/steep/annotation_parser.rb +5 -1
  8. data/lib/steep/annotations_helper.rb +12 -2
  9. data/lib/steep/ast/node/type_application.rb +22 -16
  10. data/lib/steep/ast/node/type_assertion.rb +7 -4
  11. data/lib/steep/ast/types/factory.rb +3 -2
  12. data/lib/steep/cli.rb +246 -2
  13. data/lib/steep/daemon/configuration.rb +19 -0
  14. data/lib/steep/daemon/server.rb +476 -0
  15. data/lib/steep/daemon.rb +201 -0
  16. data/lib/steep/diagnostic/ruby.rb +50 -8
  17. data/lib/steep/diagnostic/signature.rb +31 -8
  18. data/lib/steep/drivers/check.rb +301 -140
  19. data/lib/steep/drivers/print_project.rb +9 -10
  20. data/lib/steep/drivers/query.rb +102 -0
  21. data/lib/steep/drivers/start_server.rb +19 -0
  22. data/lib/steep/drivers/stop_server.rb +20 -0
  23. data/lib/steep/drivers/watch.rb +2 -2
  24. data/lib/steep/index/rbs_index.rb +38 -13
  25. data/lib/steep/index/signature_symbol_provider.rb +24 -3
  26. data/lib/steep/interface/builder.rb +48 -15
  27. data/lib/steep/interface/shape.rb +13 -5
  28. data/lib/steep/locator.rb +377 -0
  29. data/lib/steep/project/dsl.rb +26 -5
  30. data/lib/steep/project/group.rb +8 -2
  31. data/lib/steep/project/target.rb +16 -2
  32. data/lib/steep/project.rb +21 -2
  33. data/lib/steep/server/base_worker.rb +2 -2
  34. data/lib/steep/server/change_buffer.rb +2 -1
  35. data/lib/steep/server/custom_methods.rb +12 -0
  36. data/lib/steep/server/inline_source_change_detector.rb +94 -0
  37. data/lib/steep/server/interaction_worker.rb +51 -74
  38. data/lib/steep/server/lsp_formatter.rb +48 -12
  39. data/lib/steep/server/master.rb +100 -18
  40. data/lib/steep/server/target_group_files.rb +124 -151
  41. data/lib/steep/server/type_check_controller.rb +276 -123
  42. data/lib/steep/server/type_check_worker.rb +104 -3
  43. data/lib/steep/services/completion_provider/rbs.rb +74 -0
  44. data/lib/steep/services/completion_provider/ruby.rb +652 -0
  45. data/lib/steep/services/completion_provider/type_name.rb +243 -0
  46. data/lib/steep/services/completion_provider.rb +39 -662
  47. data/lib/steep/services/content_change.rb +14 -1
  48. data/lib/steep/services/file_loader.rb +4 -2
  49. data/lib/steep/services/goto_service.rb +271 -68
  50. data/lib/steep/services/hover_provider/content.rb +67 -0
  51. data/lib/steep/services/hover_provider/rbs.rb +8 -9
  52. data/lib/steep/services/hover_provider/ruby.rb +123 -64
  53. data/lib/steep/services/hover_provider/singleton_methods.rb +4 -0
  54. data/lib/steep/services/signature_service.rb +129 -54
  55. data/lib/steep/services/type_check_service.rb +72 -27
  56. data/lib/steep/signature/validator.rb +30 -18
  57. data/lib/steep/source/ignore_ranges.rb +14 -4
  58. data/lib/steep/source.rb +16 -2
  59. data/lib/steep/tagged_logging.rb +39 -0
  60. data/lib/steep/type_construction.rb +94 -21
  61. data/lib/steep/type_inference/block_params.rb +7 -7
  62. data/lib/steep/type_inference/context.rb +4 -2
  63. data/lib/steep/type_inference/logic_type_interpreter.rb +21 -3
  64. data/lib/steep/type_inference/method_call.rb +4 -0
  65. data/lib/steep/type_inference/type_env.rb +1 -1
  66. data/lib/steep/typing.rb +0 -2
  67. data/lib/steep/version.rb +1 -1
  68. data/lib/steep.rb +42 -32
  69. data/manual/ruby-diagnostics.md +67 -0
  70. data/sample/Steepfile +1 -0
  71. data/sample/lib/conference.rb +1 -0
  72. data/sample/lib/deprecated.rb +6 -0
  73. data/sample/lib/inline.rb +43 -0
  74. data/sample/sig/generics.rbs +3 -0
  75. data/steep.gemspec +4 -5
  76. metadata +26 -26
  77. data/lib/steep/services/type_name_completion.rb +0 -236
@@ -17,9 +17,18 @@ module Steep
17
17
  attr_accessor :validate_project_signatures
18
18
  attr_accessor :validate_library_signatures
19
19
  attr_accessor :formatter
20
+ attr_accessor :use_daemon
21
+ attr_reader :expressions
20
22
 
21
23
  include Utils::DriverHelper
22
24
 
25
+ PLURALIZE = {
26
+ "diagnostic" => "diagnostics",
27
+ "expression" => "expressions",
28
+ "file" => "files",
29
+ "problem" => "problems",
30
+ }.freeze
31
+
23
32
  def initialize(stdout:, stderr:)
24
33
  @stdout = stdout
25
34
  @stderr = stderr
@@ -32,6 +41,8 @@ module Steep
32
41
  @validate_project_signatures = false
33
42
  @validate_library_signatures = false
34
43
  @formatter = 'code'
44
+ @use_daemon = true
45
+ @expressions = []
35
46
  end
36
47
 
37
48
  def active_group?(group)
@@ -53,178 +64,121 @@ module Steep
53
64
  def run
54
65
  project = load_config()
55
66
 
56
- stdout.puts Rainbow("# Type checking files:").bold
57
- stdout.puts
67
+ unless expressions.empty?
68
+ return run_expressions(project)
69
+ end
58
70
 
59
- client_read, server_write = IO.pipe
60
- server_read, client_write = IO.pipe
71
+ params = build_typecheck_params(project)
61
72
 
62
- client_reader = LSP::Transport::Io::Reader.new(client_read)
63
- client_writer = LSP::Transport::Io::Writer.new(client_write)
73
+ diagnostic_notifications = [] #: Array[LanguageServer::Protocol::Interface::PublishDiagnosticsParams]
74
+ error_messages = [] #: Array[String]
64
75
 
65
- server_reader = LSP::Transport::Io::Reader.new(server_read)
66
- server_writer = LSP::Transport::Io::Writer.new(server_write)
76
+ setup_connection(project) do |reader, writer|
77
+ request_guid = SecureRandom.uuid
78
+ writer.write(Server::CustomMethods::TypeCheck.request(request_guid, params))
79
+
80
+ wait_for_response_id(reader: reader, id: request_guid) do |message|
81
+ case message[:method]
82
+ when "textDocument/publishDiagnostics"
83
+ ds = message[:params][:diagnostics]
84
+ ds.select! { |d| keep_diagnostic?(d, severity_level: severity_level) }
85
+ stdout.print(ds.empty? ? "." : "F")
86
+ diagnostic_notifications << message[:params]
87
+ stdout.flush
88
+ when "window/showMessage"
89
+ if message[:params][:type] == LSP::Constant::MessageType::ERROR
90
+ error_messages << message[:params][:message]
91
+ end
92
+ end
93
+ end
94
+ end
67
95
 
68
- typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
69
- steepfile: project.steepfile_path,
70
- args: command_line_patterns,
71
- delay_shutdown: true,
72
- steep_command: jobs_option.steep_command,
73
- count: jobs_option.jobs_count_value
74
- )
96
+ stdout.puts
97
+ stdout.puts
75
98
 
76
- master = Server::Master.new(
77
- project: project,
78
- reader: server_reader,
79
- writer: server_writer,
80
- interaction_worker: nil,
81
- typecheck_workers: typecheck_workers
82
- )
83
- master.typecheck_automatically = false
84
- master.commandline_args.push(*command_line_patterns)
99
+ print_typecheck_result(project: project, diagnostic_notifications: diagnostic_notifications, error_messages: error_messages)
100
+ rescue Errno::EPIPE => error
101
+ stdout.puts Rainbow("Steep shutdown with an error: #{error.inspect}").red.bold
102
+ 1
103
+ end
85
104
 
86
- main_thread = Thread.start do
87
- Thread.current.abort_on_exception = true
88
- master.start()
89
- end
105
+ def run_expressions(project)
106
+ target = project.targets.first or raise "No targets configured"
90
107
 
91
- Steep.logger.info { "Initializing server" }
92
- initialize_id = request_id()
93
- client_writer.write({ method: :initialize, id: initialize_id, params: DEFAULT_CLI_LSP_INITIALIZE_PARAMS })
94
- wait_for_response_id(reader: client_reader, id: initialize_id)
108
+ stdout.puts Rainbow("# Type checking expression:").bold
109
+ stdout.puts
95
110
 
96
- params = { library_paths: [], signature_paths: [], code_paths: [] } #: Server::CustomMethods::TypeCheck::params
111
+ loader = Project::Target.construct_env_loader(options: target.options, project: project)
112
+ signature_service = Services::SignatureService.load_from(loader, implicitly_returns_nil: target.implicitly_returns_nil)
113
+ subtyping = signature_service.current_subtyping or raise "Failed to build subtyping"
97
114
 
98
- if command_line_patterns.empty?
99
- files = Server::TargetGroupFiles.new(project)
100
- loader = Services::FileLoader.new(base_dir: project.base_dir)
115
+ lsp_formatter = Diagnostic::LSPFormatter.new(target.code_diagnostics_config)
116
+ total = 0
101
117
 
102
- project.targets.each do |target|
103
- target.new_env_loader.each_dir do |_, dir|
104
- RBS::FileFinder.each_file(dir, skip_hidden: true) do |path|
105
- files.add_library_path(target, path)
106
- end
107
- end
118
+ expressions.each_with_index do |expr, index|
119
+ expr_path = Pathname(expressions.size > 1 ? "(expression:#{index})" : "(expression)")
108
120
 
109
- loader.each_path_in_target(target) do |path|
110
- files.add_path(path)
111
- end
121
+ begin
122
+ source = Source.parse(expr, path: expr_path, factory: subtyping.factory)
123
+ rescue ::Parser::SyntaxError => exn
124
+ stdout.puts Rainbow("Syntax error in #{expr_path}: #{exn.message}").red.bold
125
+ total += 1
126
+ next
112
127
  end
113
128
 
114
- project.targets.each do |target|
115
- target.groups.each do |group|
116
- if active_group?(group)
117
- load_files(files, group.target, group, params: params)
118
- end
119
- end
120
- if active_group?(target)
121
- load_files(files, target, target, params: params)
122
- end
123
- end
124
- else
125
- command_line_patterns.each do |pattern|
126
- path = Pathname(pattern)
127
- path = project.absolute_path(path)
128
- next unless path.file?
129
- if target = project.target_for_source_path(path)
130
- params[:code_paths] << [target.name.to_s, path.to_s]
131
- end
132
- if target = project.target_for_signature_path(path)
133
- params[:signature_paths] << [target.name.to_s, path.to_s]
134
- end
135
- end
136
- end
129
+ typing = Services::TypeCheckService.type_check(
130
+ source: source,
131
+ subtyping: subtyping,
132
+ constant_resolver: signature_service.latest_constant_resolver,
133
+ cursor: nil
134
+ )
137
135
 
138
- Steep.logger.info { "Starting type check with #{params[:code_paths].size} Ruby files and #{params[:signature_paths].size} RBS signatures..." }
139
- Steep.logger.debug { params.inspect }
136
+ diagnostics = typing.errors.filter_map { |error| lsp_formatter.format(error) }
137
+ diagnostics.select! { |d| keep_diagnostic?(d, severity_level: severity_level) }
140
138
 
141
- request_guid = SecureRandom.uuid
142
- Steep.logger.info { "Starting type checking: #{request_guid}" }
143
- client_writer.write(Server::CustomMethods::TypeCheck.request(request_guid, params))
139
+ unless diagnostics.empty?
140
+ buffer = RBS::Buffer.new(name: expr_path, content: expr)
141
+ printer = DiagnosticPrinter.new(buffer: buffer, stdout: stdout, formatter: self.formatter)
144
142
 
145
- diagnostic_notifications = [] #: Array[LanguageServer::Protocol::Interface::PublishDiagnosticsParams]
146
- error_messages = [] #: Array[String]
147
-
148
- response = wait_for_response_id(reader: client_reader, id: request_guid) do |message|
149
- case
150
- when message[:method] == "textDocument/publishDiagnostics"
151
- ds = message[:params][:diagnostics]
152
- ds.select! {|d| keep_diagnostic?(d, severity_level: severity_level) }
153
- if ds.empty?
154
- stdout.print "."
155
- else
156
- stdout.print "F"
157
- end
158
- diagnostic_notifications << message[:params]
159
- stdout.flush
160
- when message[:method] == "window/showMessage"
161
- # Assuming ERROR message means unrecoverable error.
162
- message = message[:params]
163
- if message[:type] == LSP::Constant::MessageType::ERROR
164
- error_messages << message[:message]
143
+ diagnostics.each do |diag|
144
+ printer.print(diag)
145
+ stdout.puts
165
146
  end
147
+
148
+ total += diagnostics.size
166
149
  end
167
150
  end
168
151
 
169
- Steep.logger.info { "Finished type checking: #{response.inspect}" }
170
-
171
- Steep.logger.info { "Shutting down..." }
172
-
173
- shutdown_exit(reader: client_reader, writer: client_writer)
174
- main_thread.join()
175
-
176
- stdout.puts
177
- stdout.puts
178
-
179
- if error_messages.empty?
180
- loader = Services::FileLoader.new(base_dir: project.base_dir)
181
- all_files = project.targets.each.with_object(Set[]) do |target, set|
182
- set.merge(loader.load_changes(target.source_pattern, command_line_patterns, changes: {}).each_key)
183
- set.merge(loader.load_changes(target.signature_pattern, changes: {}).each_key)
184
- end.to_a
185
-
186
- case
187
- when with_expectations_path
188
- print_expectations(project: project,
189
- all_files: all_files,
190
- expectations_path: with_expectations_path,
191
- notifications: diagnostic_notifications)
192
- when save_expectations_path
193
- save_expectations(project: project,
194
- all_files: all_files,
195
- expectations_path: save_expectations_path,
196
- notifications: diagnostic_notifications)
197
- else
198
- print_result(project: project, notifications: diagnostic_notifications)
199
- end
152
+ if total == 0
153
+ emoji = %w(🫖 🫖 🫖 🫖 🫖 🫖 🫖 🫖 🍵 🧋 🧉).sample
154
+ stdout.puts Rainbow("No type error detected. #{emoji}").green.bold
155
+ 0
200
156
  else
201
- stdout.puts Rainbow("Unexpected error reported. 🚨").red.bold
157
+ stdout.puts Rainbow("Detected #{total} #{pluralize("problem", total)} from #{expressions.size} #{pluralize("expression", expressions.size)}").red.bold
202
158
  1
203
159
  end
204
- rescue Errno::EPIPE => error
205
- stdout.puts Rainbow("Steep shutdown with an error: #{error.inspect}").red.bold
206
- return 1
207
160
  end
208
161
 
209
162
  def load_files(files, target, group, params:)
210
163
  if type_check_code
211
- files.each_group_source_path(group, true) do |path|
164
+ files.source_paths.each_group_path(group) do |path,|
212
165
  params[:code_paths] << [target.name.to_s, target.project.absolute_path(path).to_s]
213
166
  end
167
+ files.inline_paths.each_group_path(group) do |path,|
168
+ params[:inline_paths] << [target.name.to_s, target.project.absolute_path(path).to_s]
169
+ end
214
170
  end
215
171
  if validate_group_signatures
216
- files.each_group_signature_path(group) do |path|
172
+ files.signature_paths.each_group_path(group) do |path,|
217
173
  params[:signature_paths] << [target.name.to_s, target.project.absolute_path(path).to_s]
218
174
  end
219
175
  end
220
176
  if validate_project_signatures
221
- files.each_project_signature_path(target) do |path|
222
- if path_target = files.signature_path_target(path)
223
- params[:signature_paths] << [path_target.name.to_s, target.project.absolute_path(path).to_s]
224
- end
177
+ files.signature_paths.each_project_path(except: target) do |path, path_target|
178
+ params[:signature_paths] << [path_target.name.to_s, target.project.absolute_path(path).to_s]
225
179
  end
226
180
  if group.is_a?(Project::Group)
227
- files.each_target_signature_path(target, group) do |path|
181
+ files.signature_paths.each_target_path(target, except: group) do |path,|
228
182
  params[:signature_paths] << [target.name.to_s, target.project.absolute_path(path).to_s]
229
183
  end
230
184
  end
@@ -274,13 +228,13 @@ module Steep
274
228
  stdout.puts
275
229
 
276
230
  stdout.puts Rainbow("Expectations unsatisfied:").bold.red
277
- stdout.puts " #{expected_count} expected #{"diagnostic".pluralize(expected_count)}"
278
- stdout.puts Rainbow(" + #{unexpected_count} unexpected #{"diagnostic".pluralize(unexpected_count)}").green
279
- stdout.puts Rainbow(" - #{missing_count} missing #{"diagnostic".pluralize(missing_count)}").red
231
+ stdout.puts " #{expected_count} expected #{pluralize("diagnostic", expected_count)}"
232
+ stdout.puts Rainbow(" + #{unexpected_count} unexpected #{pluralize("diagnostic", unexpected_count)}").green
233
+ stdout.puts Rainbow(" - #{missing_count} missing #{pluralize("diagnostic", missing_count)}").red
280
234
  1
281
235
  else
282
236
  stdout.puts Rainbow("Expectations satisfied:").bold.green
283
- stdout.puts " #{expected_count} expected #{"diagnostic".pluralize(expected_count)}"
237
+ stdout.puts " #{expected_count} expected #{pluralize("diagnostic", expected_count)}"
284
238
  0
285
239
  end
286
240
  end
@@ -332,10 +286,217 @@ module Steep
332
286
  end
333
287
  end
334
288
 
335
- stdout.puts Rainbow("Detected #{total} #{"problem".pluralize(total)} from #{errors.size} #{"file".pluralize(errors.size)}").red.bold
289
+ stdout.puts Rainbow("Detected #{total} #{pluralize("problem", total)} from #{errors.size} #{pluralize("file", errors.size)}").red.bold
336
290
  1
337
291
  end
338
292
  end
293
+
294
+ def pluralize(string, count)
295
+ if count == 1
296
+ string
297
+ else
298
+ PLURALIZE.fetch(string)
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ def setup_connection(project, &block)
305
+ if use_daemon && daemon_available?
306
+ begin
307
+ stdout.puts Rainbow("# Type checking files (server mode):").bold
308
+ stdout.puts
309
+ return with_daemon_connection(&block)
310
+ rescue Errno::ECONNREFUSED, Errno::ENOENT => e
311
+ stderr.puts "Steep server connection failed (#{e.message}), falling back to standard mode"
312
+ end
313
+ end
314
+
315
+ stdout.puts Rainbow("# Type checking files:").bold
316
+ stdout.puts
317
+ with_local_server(project, &block)
318
+ end
319
+
320
+ def daemon_available?
321
+ if Daemon.running?
322
+ Steep.logger.info { "Daemon detected, using server mode" }
323
+ return true
324
+ end
325
+
326
+ if Daemon.starting?
327
+ Steep.logger.info { "Daemon is starting, waiting for it to be ready" }
328
+ if wait_for_daemon
329
+ return true
330
+ else
331
+ stderr.puts Rainbow("Daemon failed to start, falling back to standard mode").yellow
332
+ end
333
+ end
334
+
335
+ false
336
+ end
337
+
338
+ def with_daemon_connection
339
+ socket = UNIXSocket.new(Daemon.socket_path)
340
+ reader = LSP::Transport::Io::Reader.new(socket)
341
+ writer = LSP::Transport::Io::Writer.new(socket)
342
+ yield reader, writer
343
+ ensure
344
+ socket&.close
345
+ end
346
+
347
+ def with_local_server(project)
348
+ client_read, server_write = IO.pipe
349
+ server_read, client_write = IO.pipe
350
+
351
+ client_reader = LSP::Transport::Io::Reader.new(client_read)
352
+ client_writer = LSP::Transport::Io::Writer.new(client_write)
353
+
354
+ server_reader = LSP::Transport::Io::Reader.new(server_read)
355
+ server_writer = LSP::Transport::Io::Writer.new(server_write)
356
+
357
+ typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
358
+ steepfile: project.steepfile_path,
359
+ args: command_line_patterns,
360
+ delay_shutdown: true,
361
+ steep_command: jobs_option.steep_command,
362
+ count: jobs_option.jobs_count_value
363
+ )
364
+
365
+ master = Server::Master.new(
366
+ project: project,
367
+ reader: server_reader,
368
+ writer: server_writer,
369
+ interaction_worker: nil,
370
+ typecheck_workers: typecheck_workers
371
+ )
372
+ master.typecheck_automatically = false
373
+ master.commandline_args.push(*command_line_patterns)
374
+
375
+ main_thread = Thread.start do
376
+ Thread.current.abort_on_exception = true
377
+ master.start()
378
+ end
379
+
380
+ Steep.logger.info { "Initializing server" }
381
+ initialize_id = request_id()
382
+ client_writer.write({ method: :initialize, id: initialize_id, params: DEFAULT_CLI_LSP_INITIALIZE_PARAMS })
383
+ wait_for_response_id(reader: client_reader, id: initialize_id)
384
+
385
+ yield client_reader, client_writer
386
+ ensure
387
+ if client_reader && client_writer
388
+ shutdown_exit(reader: client_reader, writer: client_writer)
389
+ end
390
+ main_thread&.join()
391
+ end
392
+
393
+ def print_typecheck_result(project:, diagnostic_notifications:, error_messages:)
394
+ if error_messages.empty?
395
+ loader = Services::FileLoader.new(base_dir: project.base_dir)
396
+ all_files = project.targets.each.with_object(Set[]) do |target, set|
397
+ set.merge(loader.load_changes(target.source_pattern, command_line_patterns, changes: {}).each_key)
398
+ set.merge(loader.load_changes(target.signature_pattern, changes: {}).each_key)
399
+ end.to_a
400
+
401
+ case
402
+ when with_expectations_path
403
+ print_expectations(project: project,
404
+ all_files: all_files,
405
+ expectations_path: with_expectations_path,
406
+ notifications: diagnostic_notifications)
407
+ when save_expectations_path
408
+ save_expectations(project: project,
409
+ all_files: all_files,
410
+ expectations_path: save_expectations_path,
411
+ notifications: diagnostic_notifications)
412
+ else
413
+ print_result(project: project, notifications: diagnostic_notifications)
414
+ end
415
+ else
416
+ stdout.puts Rainbow("Unexpected error reported. 🚨").red.bold
417
+ 1
418
+ end
419
+ end
420
+
421
+ def build_typecheck_params(project)
422
+ params = { library_paths: [], inline_paths: [], signature_paths: [], code_paths: [] } #: Server::CustomMethods::TypeCheck::params
423
+
424
+ if command_line_patterns.empty?
425
+ files = Server::TargetGroupFiles.new(project)
426
+ loader = Services::FileLoader.new(base_dir: project.base_dir)
427
+
428
+ project.targets.each do |target|
429
+ target.new_env_loader.each_dir do |_, dir|
430
+ RBS::FileFinder.each_file(dir, skip_hidden: true) do |path|
431
+ files.add_library_path(target, path)
432
+ end
433
+ end
434
+
435
+ loader.each_path_in_target(target) do |path|
436
+ files.add_path(path)
437
+ end
438
+ end
439
+
440
+ project.targets.each do |target|
441
+ target.groups.each do |group|
442
+ if active_group?(group)
443
+ load_files(files, target, group, params: params)
444
+ end
445
+ end
446
+ if active_group?(target)
447
+ load_files(files, target, target, params: params)
448
+ end
449
+ end
450
+ else
451
+ command_line_patterns.each do |pattern|
452
+ path = Pathname(pattern)
453
+ path = project.absolute_path(path)
454
+ next unless path.file?
455
+ if target = project.target_for_source_path(path)
456
+ params[:code_paths] << [target.name.to_s, path.to_s]
457
+ end
458
+ if target = project.target_for_signature_path(path)
459
+ params[:signature_paths] << [target.name.to_s, path.to_s]
460
+ end
461
+ if target = project.target_for_inline_source_path(path)
462
+ params[:inline_paths] << [target.name.to_s, path.to_s]
463
+ end
464
+ end
465
+ end
466
+
467
+ params
468
+ end
469
+
470
+ def wait_for_daemon(timeout: 300)
471
+ stdout.puts "Daemon is warming up, waiting for it to be ready..."
472
+ start_time = Time.now
473
+ dots_printed = 0
474
+
475
+ loop do
476
+ if Daemon.running?
477
+ stdout.puts unless dots_printed == 0
478
+ return true
479
+ end
480
+
481
+ elapsed = Time.now - start_time
482
+ if elapsed > timeout
483
+ stdout.puts unless dots_printed == 0
484
+ Steep.logger.warn { "Daemon warm-up timed out after #{timeout}s" }
485
+ return false
486
+ end
487
+
488
+ unless Daemon.starting?
489
+ stdout.puts unless dots_printed == 0
490
+ Steep.logger.warn { "Daemon process died during warm-up" }
491
+ return false
492
+ end
493
+
494
+ sleep 1
495
+ stdout.print "."
496
+ stdout.flush
497
+ dots_printed += 1
498
+ end
499
+ end
339
500
  end
340
501
  end
341
502
  end
@@ -1,5 +1,3 @@
1
- require "active_support/core_ext/hash/keys"
2
-
3
1
  module Steep
4
2
  module Drivers
5
3
  class PrintProject
@@ -19,17 +17,18 @@ module Steep
19
17
 
20
18
  def as_json(project)
21
19
  {
22
- steepfile: project.steepfile_path.to_s,
23
- targets: project.targets.map do |target|
20
+ "steepfile" => project.steepfile_path.to_s,
21
+ "targets" => project.targets.map do |target|
24
22
  target_as_json(target)
25
23
  end
26
- }.stringify_keys
24
+ }
27
25
  end
28
26
 
29
27
  def target_as_json(target)
30
28
  json = {
31
29
  "name" => target.name.to_s,
32
30
  "source_pattern" => pattern_as_json(target.source_pattern),
31
+ "inline_source_pattern" => pattern_as_json(target.inline_source_pattern),
33
32
  "signature_pattern" => pattern_as_json(target.signature_pattern),
34
33
  "groups" => target.groups.map do |group|
35
34
  group_as_json(group)
@@ -52,10 +51,10 @@ module Steep
52
51
  } #: target_json
53
52
 
54
53
  if files
55
- files.each_group_signature_path(target, true) do |path|
54
+ files.signature_paths.each_group_path(target) do |path,|
56
55
  (json["signature_paths"] ||= []) << path.to_s
57
56
  end
58
- files.each_group_source_path(target, true) do |path|
57
+ files.source_paths.each_group_path(target) do |path,|
59
58
  (json["source_paths"] ||= []) << path.to_s
60
59
  end
61
60
  end
@@ -71,11 +70,11 @@ module Steep
71
70
  } #: group_json
72
71
 
73
72
  if files
74
- files.each_group_signature_path(group, true) do |path|
73
+ files.signature_paths.each_group_path(group) do |path,|
75
74
  (json["signature_paths"] ||= []) << path.to_s
76
-
77
75
  end
78
- files.each_group_source_path(group, true) do |path|
76
+
77
+ files.source_paths.each_group_path(group) do |path,|
79
78
  (json["source_paths"] ||= []) << path.to_s
80
79
  end
81
80
  end
@@ -0,0 +1,102 @@
1
+ module Steep
2
+ module Drivers
3
+ class Query
4
+ LSP = LanguageServer::Protocol
5
+
6
+ attr_reader :stdout
7
+ attr_reader :stderr
8
+
9
+ def initialize(stdout:, stderr:)
10
+ @stdout = stdout
11
+ @stderr = stderr
12
+ end
13
+
14
+ # @rbs locations: Array[[String, Integer, Integer]] -- array of [path, line, column]
15
+ # @rbs return: Integer
16
+ def run_hover(locations:)
17
+ unless Daemon.running?
18
+ stderr.puts "Error: Steep daemon is not running. Start it with `steep server start`."
19
+ return 1
20
+ end
21
+
22
+ locations.each do |path, line, column|
23
+ absolute_path = to_absolute_path(path)
24
+ unless absolute_path
25
+ stdout.puts JSON.generate({ file: path, line: line, column: column, error: "File not found: #{path}" })
26
+ next
27
+ end
28
+
29
+ uri = PathHelper.to_uri(absolute_path).to_s
30
+
31
+ request = {
32
+ id: SecureRandom.uuid,
33
+ method: "textDocument/hover",
34
+ params: {
35
+ textDocument: { uri: uri },
36
+ position: { line: line - 1, character: column - 1 }
37
+ }
38
+ }
39
+
40
+ result = send_request(request)
41
+ stdout.puts JSON.generate({ file: path, line: line, column: column, result: result })
42
+ end
43
+
44
+ 0
45
+ rescue Errno::ECONNREFUSED, Errno::ENOENT => e
46
+ stderr.puts "Error: Failed to connect to Steep daemon: #{e.message}"
47
+ 1
48
+ end
49
+
50
+ # @rbs names: Array[String]
51
+ # @rbs return: Integer
52
+ def run_definition(names:)
53
+ unless Daemon.running?
54
+ stderr.puts "Error: Steep daemon is not running. Start it with `steep server start`."
55
+ return 1
56
+ end
57
+
58
+ names.each do |name|
59
+ request = {
60
+ id: SecureRandom.uuid,
61
+ method: Server::CustomMethods::Query__Definition::METHOD,
62
+ params: { name: name }
63
+ }
64
+
65
+ result = send_request(request)
66
+ stdout.puts JSON.generate({ name: name, result: result })
67
+ end
68
+
69
+ 0
70
+ rescue Errno::ECONNREFUSED, Errno::ENOENT => e
71
+ stderr.puts "Error: Failed to connect to Steep daemon: #{e.message}"
72
+ 1
73
+ end
74
+
75
+ private
76
+
77
+ def to_absolute_path(path)
78
+ pathname = Pathname(path)
79
+ pathname = Pathname.pwd + pathname unless pathname.absolute?
80
+ pathname.file? ? pathname : nil
81
+ end
82
+
83
+ def send_request(request)
84
+ socket = UNIXSocket.new(Daemon.socket_path)
85
+ reader = LSP::Transport::Io::Reader.new(socket)
86
+ writer = LSP::Transport::Io::Writer.new(socket)
87
+
88
+ writer.write(request)
89
+
90
+ result = nil #: untyped
91
+ reader.read do |message|
92
+ result = message[:result]
93
+ break
94
+ end
95
+
96
+ result
97
+ ensure
98
+ socket&.close
99
+ end
100
+ end
101
+ end
102
+ end