steep 1.9.4 → 1.10.0.pre.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -3
  3. data/Steepfile +5 -2
  4. data/lib/steep/annotations_helper.rb +43 -0
  5. data/lib/steep/ast/ignore.rb +3 -4
  6. data/lib/steep/cli.rb +102 -23
  7. data/lib/steep/diagnostic/lsp_formatter.rb +17 -3
  8. data/lib/steep/diagnostic/ruby.rb +75 -0
  9. data/lib/steep/diagnostic/signature.rb +20 -0
  10. data/lib/steep/drivers/check.rb +3 -1
  11. data/lib/steep/drivers/diagnostic_printer/base_formatter.rb +19 -0
  12. data/lib/steep/drivers/diagnostic_printer/code_formatter.rb +95 -0
  13. data/lib/steep/drivers/diagnostic_printer/github_actions_formatter.rb +44 -0
  14. data/lib/steep/drivers/diagnostic_printer.rb +9 -86
  15. data/lib/steep/drivers/langserver.rb +4 -1
  16. data/lib/steep/drivers/worker.rb +2 -0
  17. data/lib/steep/interface/builder.rb +2 -2
  18. data/lib/steep/server/custom_methods.rb +12 -0
  19. data/lib/steep/server/interaction_worker.rb +33 -6
  20. data/lib/steep/server/master.rb +103 -7
  21. data/lib/steep/server/type_check_worker.rb +48 -2
  22. data/lib/steep/server/worker_process.rb +31 -20
  23. data/lib/steep/services/completion_provider.rb +12 -2
  24. data/lib/steep/services/signature_service.rb +2 -2
  25. data/lib/steep/services/type_check_service.rb +22 -7
  26. data/lib/steep/signature/validator.rb +56 -4
  27. data/lib/steep/source.rb +3 -1
  28. data/lib/steep/subtyping/check.rb +22 -16
  29. data/lib/steep/type_construction.rb +153 -34
  30. data/lib/steep/type_inference/case_when.rb +2 -0
  31. data/lib/steep/type_inference/logic_type_interpreter.rb +77 -15
  32. data/lib/steep/type_inference/method_call.rb +5 -3
  33. data/lib/steep/version.rb +1 -1
  34. data/lib/steep.rb +10 -0
  35. data/manual/ruby-diagnostics.md +114 -1
  36. data/sample/Steepfile +0 -2
  37. data/sample/lib/conference.rb +14 -0
  38. data/sample/lib/deprecated.rb +7 -0
  39. data/sample/sig/deprecated.rbs +16 -0
  40. data/steep.gemspec +4 -3
  41. metadata +28 -8
@@ -0,0 +1,95 @@
1
+ module Steep
2
+ module Drivers
3
+ class DiagnosticPrinter
4
+ class CodeFormatter < BaseFormatter
5
+ def print(diagnostic, prefix: "", source: true)
6
+ header, *rest = diagnostic[:message].split(/\n/)
7
+
8
+ stdout.puts "#{prefix}#{location(diagnostic)}: [#{severity_message(diagnostic[:severity])}] #{Rainbow(header).underline}"
9
+
10
+ unless rest.empty?
11
+ rest.each do |message|
12
+ stdout.puts "#{prefix}│ #{message}"
13
+ end
14
+ end
15
+
16
+ if diagnostic[:code]
17
+ stdout.puts "#{prefix}│" unless rest.empty?
18
+ stdout.puts "#{prefix}│ Diagnostic ID: #{diagnostic[:code]}"
19
+ end
20
+
21
+ stdout.puts "#{prefix}│"
22
+
23
+ if source
24
+ print_source_line(diagnostic, prefix: prefix)
25
+ else
26
+ stdout.puts "#{prefix}└ (no source code available)"
27
+ stdout.puts "#{prefix}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def color_severity(string, severity:)
34
+ s = Rainbow(string)
35
+
36
+ case severity
37
+ when LSP::Constant::DiagnosticSeverity::ERROR
38
+ s.red
39
+ when LSP::Constant::DiagnosticSeverity::WARNING
40
+ s.yellow
41
+ when LSP::Constant::DiagnosticSeverity::INFORMATION
42
+ s.blue
43
+ else
44
+ s
45
+ end
46
+ end
47
+
48
+ def severity_message(severity)
49
+ string = case severity
50
+ when LSP::Constant::DiagnosticSeverity::ERROR
51
+ "error"
52
+ when LSP::Constant::DiagnosticSeverity::WARNING
53
+ "warning"
54
+ when LSP::Constant::DiagnosticSeverity::INFORMATION
55
+ "information"
56
+ when LSP::Constant::DiagnosticSeverity::HINT
57
+ "hint"
58
+ else
59
+ raise
60
+ end
61
+
62
+ color_severity(string, severity: severity)
63
+ end
64
+
65
+ def location(diagnostic)
66
+ start = diagnostic[:range][:start]
67
+ Rainbow("#{path}:#{start[:line]+1}:#{start[:character]}").magenta
68
+ end
69
+
70
+ def print_source_line(diagnostic, prefix: "")
71
+ start_pos = diagnostic[:range][:start]
72
+ end_pos = diagnostic[:range][:end]
73
+
74
+ line = buffer.lines.fetch(start_pos[:line])
75
+
76
+ leading = line[0...start_pos[:character]] || ""
77
+ if start_pos[:line] == end_pos[:line]
78
+ subject = line[start_pos[:character]...end_pos[:character]] || ""
79
+ trailing = (line[end_pos[:character]...] || "").chomp
80
+ else
81
+ subject = (line[start_pos[:character]...] || "").chomp
82
+ trailing = ""
83
+ end
84
+
85
+ unless subject.valid_encoding?
86
+ subject.scrub!
87
+ end
88
+
89
+ stdout.puts "#{prefix}└ #{leading}#{color_severity(subject, severity: diagnostic[:severity])}#{trailing}"
90
+ stdout.puts "#{prefix} #{" " * leading.size}#{"~" * subject.size}"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,44 @@
1
+ module Steep
2
+ module Drivers
3
+ class DiagnosticPrinter
4
+ # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions
5
+ class GitHubActionsFormatter < BaseFormatter
6
+ ESCAPE_MAP = { '%' => '%25', "\n" => '%0A', "\r" => '%0D' }.freeze
7
+
8
+ def print(diagnostic, prefix: "", source: true)
9
+ stdout.printf(
10
+ "::%<severity>s file=%<file>s,line=%<line>d,endLine=%<endLine>d,col=%<column>d,endColumn=%<endColumn>d::%<message>s",
11
+ severity: github_severity(diagnostic[:severity]),
12
+ file: path,
13
+ line: diagnostic[:range][:start][:line] + 1,
14
+ endLine: diagnostic[:range][:end][:line] + 1,
15
+ column: diagnostic[:range][:start][:character],
16
+ endColumn: diagnostic[:range][:end][:character],
17
+ message: github_escape("[#{diagnostic[:code]}] #{diagnostic[:message]}")
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def github_severity(severity)
24
+ case severity
25
+ when LSP::Constant::DiagnosticSeverity::ERROR
26
+ "error"
27
+ when LSP::Constant::DiagnosticSeverity::WARNING
28
+ "warning"
29
+ when LSP::Constant::DiagnosticSeverity::INFORMATION
30
+ "notice"
31
+ when LSP::Constant::DiagnosticSeverity::HINT
32
+ "notice"
33
+ else
34
+ raise
35
+ end
36
+ end
37
+
38
+ def github_escape(string)
39
+ string.gsub(Regexp.union(ESCAPE_MAP.keys), ESCAPE_MAP)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,104 +1,27 @@
1
1
  module Steep
2
2
  module Drivers
3
3
  class DiagnosticPrinter
4
+
4
5
  LSP = LanguageServer::Protocol
5
6
 
6
7
  attr_reader :stdout
7
8
  attr_reader :buffer
8
9
 
9
- def initialize(stdout:, buffer:)
10
+ def initialize(stdout:, buffer:, formatter: 'code')
10
11
  @stdout = stdout
11
12
  @buffer = buffer
12
- end
13
-
14
- def path
15
- Pathname(buffer.name)
16
- end
17
-
18
- def color_severity(string, severity:)
19
- s = Rainbow(string)
20
-
21
- case severity
22
- when LSP::Constant::DiagnosticSeverity::ERROR
23
- s.red
24
- when LSP::Constant::DiagnosticSeverity::WARNING
25
- s.yellow
26
- when LSP::Constant::DiagnosticSeverity::INFORMATION
27
- s.blue
13
+ @formatter = case formatter
14
+ when 'code'
15
+ CodeFormatter.new(stdout: stdout, buffer: buffer)
16
+ when 'github'
17
+ GitHubActionsFormatter.new(stdout: stdout, buffer: buffer)
28
18
  else
29
- s
19
+ raise "Unknown formatter: #{formatter}"
30
20
  end
31
21
  end
32
22
 
33
- def severity_message(severity)
34
- string = case severity
35
- when LSP::Constant::DiagnosticSeverity::ERROR
36
- "error"
37
- when LSP::Constant::DiagnosticSeverity::WARNING
38
- "warning"
39
- when LSP::Constant::DiagnosticSeverity::INFORMATION
40
- "information"
41
- when LSP::Constant::DiagnosticSeverity::HINT
42
- "hint"
43
- else
44
- raise
45
- end
46
-
47
- color_severity(string, severity: severity)
48
- end
49
-
50
- def location(diagnostic)
51
- start = diagnostic[:range][:start]
52
- Rainbow("#{path}:#{start[:line]+1}:#{start[:character]}").magenta
53
- end
54
-
55
23
  def print(diagnostic, prefix: "", source: true)
56
- header, *rest = diagnostic[:message].split(/\n/)
57
-
58
- stdout.puts "#{prefix}#{location(diagnostic)}: [#{severity_message(diagnostic[:severity])}] #{Rainbow(header).underline}"
59
-
60
- unless rest.empty?
61
- rest.each do |message|
62
- stdout.puts "#{prefix}│ #{message}"
63
- end
64
- end
65
-
66
- if diagnostic[:code]
67
- stdout.puts "#{prefix}│" unless rest.empty?
68
- stdout.puts "#{prefix}│ Diagnostic ID: #{diagnostic[:code]}"
69
- end
70
-
71
- stdout.puts "#{prefix}│"
72
-
73
- if source
74
- print_source_line(diagnostic, prefix: prefix)
75
- else
76
- stdout.puts "#{prefix}└ (no source code available)"
77
- stdout.puts "#{prefix}"
78
- end
79
- end
80
-
81
- def print_source_line(diagnostic, prefix: "")
82
- start_pos = diagnostic[:range][:start]
83
- end_pos = diagnostic[:range][:end]
84
-
85
- line = buffer.lines.fetch(start_pos[:line])
86
-
87
- leading = line[0...start_pos[:character]] || ""
88
- if start_pos[:line] == end_pos[:line]
89
- subject = line[start_pos[:character]...end_pos[:character]] || ""
90
- trailing = (line[end_pos[:character]...] || "").chomp
91
- else
92
- subject = (line[start_pos[:character]...] || "").chomp
93
- trailing = ""
94
- end
95
-
96
- unless subject.valid_encoding?
97
- subject.scrub!
98
- end
99
-
100
- stdout.puts "#{prefix}└ #{leading}#{color_severity(subject, severity: diagnostic[:severity])}#{trailing}"
101
- stdout.puts "#{prefix} #{" " * leading.size}#{"~" * subject.size}"
24
+ @formatter.print(diagnostic, prefix: prefix, source: source)
102
25
  end
103
26
  end
104
27
  end
@@ -8,6 +8,7 @@ module Steep
8
8
  attr_reader :type_check_queue
9
9
  attr_reader :type_check_thread
10
10
  attr_reader :jobs_option
11
+ attr_accessor :refork
11
12
 
12
13
  include Utils::DriverHelper
13
14
 
@@ -18,6 +19,7 @@ module Steep
18
19
  @write_mutex = Mutex.new
19
20
  @type_check_queue = Queue.new
20
21
  @jobs_option = Utils::JobsOption.new(jobs_count_modifier: -1)
22
+ @refork = false
21
23
  end
22
24
 
23
25
  def writer
@@ -43,7 +45,8 @@ module Steep
43
45
  reader: reader,
44
46
  writer: writer,
45
47
  interaction_worker: interaction_worker,
46
- typecheck_workers: typecheck_workers
48
+ typecheck_workers: typecheck_workers,
49
+ refork: refork,
47
50
  )
48
51
  master.typecheck_automatically = true
49
52
 
@@ -9,6 +9,7 @@ module Steep
9
9
  attr_accessor :max_index
10
10
  attr_accessor :index
11
11
  attr_accessor :commandline_args
12
+ attr_accessor :io_socket
12
13
 
13
14
  include Utils::DriverHelper
14
15
 
@@ -32,6 +33,7 @@ module Steep
32
33
  Server::TypeCheckWorker.new(project: project,
33
34
  reader: reader,
34
35
  writer: writer,
36
+ io_socket:,
35
37
  assignment: assignment,
36
38
  commandline_args: commandline_args)
37
39
  when :interaction
@@ -283,7 +283,7 @@ module Steep
283
283
  method_type = factory.method_type(type_def.type)
284
284
  method_type = replace_primitive_method(method_name, type_def, method_type)
285
285
  method_type = replace_kernel_class(method_name, type_def, method_type) { AST::Builtin::Class.instance_type }
286
- method_type = add_implicitly_returns_nil(type_def.annotations, method_type)
286
+ method_type = add_implicitly_returns_nil(type_def.each_annotation, method_type)
287
287
  Shape::MethodOverload.new(method_type, [type_def])
288
288
  end
289
289
 
@@ -317,7 +317,7 @@ module Steep
317
317
  if type_name.class?
318
318
  method_type = replace_kernel_class(method_name, type_def, method_type) { AST::Types::Name::Singleton.new(name: type_name) }
319
319
  end
320
- method_type = add_implicitly_returns_nil(type_def.annotations, method_type)
320
+ method_type = add_implicitly_returns_nil(type_def.each_annotation, method_type)
321
321
  Shape::MethodOverload.new(method_type, [type_def])
322
322
  end
323
323
 
@@ -72,6 +72,18 @@ module Steep
72
72
  { id: id, result: result }
73
73
  end
74
74
  end
75
+
76
+ module Refork
77
+ METHOD = "$/steep/refork"
78
+
79
+ def self.request(id, params)
80
+ { method: METHOD, id: id, params: params }
81
+ end
82
+
83
+ def self.response(id, result)
84
+ { id: id, result: result }
85
+ end
86
+ end
75
87
  end
76
88
  end
77
89
  end
@@ -268,6 +268,11 @@ module Steep
268
268
 
269
269
  type_name = sig_service.latest_env.normalize_type_name(type_name)
270
270
 
271
+ tags = [] #: Array[LSP::Constant::CompletionItemTag::t]
272
+ if AnnotationsHelper.deprecated_type_name?(type_name, sig_service.latest_env)
273
+ tags << LSP::Constant::CompletionItemTag::DEPRECATED
274
+ end
275
+
271
276
  case type_name.kind
272
277
  when :class
273
278
  env = sig_service.latest_env
@@ -290,7 +295,8 @@ module Steep
290
295
  range: range,
291
296
  new_text: complete_text
292
297
  ),
293
- kind: LSP::Constant::CompletionItemKind::CLASS
298
+ kind: LSP::Constant::CompletionItemKind::CLASS,
299
+ tags: tags
294
300
  )
295
301
  when :alias
296
302
  alias_decl = sig_service.latest_env.type_alias_decls[type_name]&.decl or raise
@@ -303,7 +309,8 @@ module Steep
303
309
  new_text: complete_text
304
310
  ),
305
311
  documentation: LSPFormatter.markup_content { LSPFormatter.format_rbs_completion_docs(type_name, alias_decl, [alias_decl.comment].compact) },
306
- kind: LSP::Constant::CompletionItemKind::FIELD
312
+ kind: LSP::Constant::CompletionItemKind::FIELD,
313
+ tags: tags
307
314
  )
308
315
  when :interface
309
316
  interface_decl = sig_service.latest_env.interface_decls[type_name]&.decl or raise
@@ -316,7 +323,8 @@ module Steep
316
323
  new_text: complete_text
317
324
  ),
318
325
  documentation: LSPFormatter.markup_content { LSPFormatter.format_rbs_completion_docs(type_name, interface_decl, [interface_decl.comment].compact) },
319
- kind: LSP::Constant::CompletionItemKind::INTERFACE
326
+ kind: LSP::Constant::CompletionItemKind::INTERFACE,
327
+ tags: tags
320
328
  )
321
329
  else
322
330
  raise
@@ -355,6 +363,11 @@ module Steep
355
363
 
356
364
  detail = LSPFormatter.declaration_summary(item.decl)
357
365
 
366
+ tags = [] #: Array[LSP::Constant::CompletionItemTag::t]
367
+ if item.deprecated?
368
+ tags << LSP::Constant::CompletionItemTag::DEPRECATED
369
+ end
370
+
358
371
  LSP::Interface::CompletionItem.new(
359
372
  label: item.identifier.to_s,
360
373
  kind: kind,
@@ -363,15 +376,22 @@ module Steep
363
376
  text_edit: LSP::Interface::TextEdit.new(
364
377
  range: range,
365
378
  new_text: item.identifier.to_s
366
- )
379
+ ),
380
+ tags: tags
367
381
  )
368
382
  when Services::CompletionProvider::SimpleMethodNameItem
383
+ tags = [] #: Array[LSP::Constant::CompletionItemTag::t]
384
+ if item.deprecated
385
+ tags << LSP::Constant::CompletionItemTag::DEPRECATED
386
+ end
387
+
369
388
  LSP::Interface::CompletionItem.new(
370
389
  label: item.identifier.to_s,
371
390
  kind: LSP::Constant::CompletionItemKind::FUNCTION,
372
391
  label_details: LSP::Interface::CompletionItemLabelDetails.new(description: item.method_name.relative.to_s),
373
392
  insert_text: item.identifier.to_s,
374
- documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) }
393
+ documentation: LSPFormatter.markup_content { LSPFormatter.format_completion_docs(item) },
394
+ tags: tags
375
395
  )
376
396
  when Services::CompletionProvider::ComplexMethodNameItem
377
397
  method_names = item.method_names.map(&:relative).uniq
@@ -423,6 +443,12 @@ module Steep
423
443
  when item.absolute_type_name.alias?
424
444
  LSP::Constant::CompletionItemKind::FIELD
425
445
  end
446
+
447
+ tags = [] #: Array[LSP::Constant::CompletionItemTag::t]
448
+ if AnnotationsHelper.deprecated_type_name?(item.absolute_type_name, item.env)
449
+ tags << LSP::Constant::CompletionItemTag::DEPRECATED
450
+ end
451
+
426
452
  LSP::Interface::CompletionItem.new(
427
453
  label: item.relative_type_name.to_s,
428
454
  kind: kind,
@@ -431,7 +457,8 @@ module Steep
431
457
  text_edit: LSP::Interface::TextEdit.new(
432
458
  range: range,
433
459
  new_text: item.relative_type_name.to_s
434
- )
460
+ ),
461
+ tags: tags
435
462
  )
436
463
  when Services::CompletionProvider::TextItem
437
464
  LSP::Interface::CompletionItem.new(
@@ -178,6 +178,7 @@ module Steep
178
178
  attr_reader :job_queue, :write_queue
179
179
 
180
180
  attr_reader :current_type_check_request
181
+ attr_reader :refork_mutex
181
182
  attr_reader :controller
182
183
  attr_reader :result_controller
183
184
 
@@ -185,7 +186,7 @@ module Steep
185
186
  attr_accessor :typecheck_automatically
186
187
  attr_reader :start_type_checking_queue
187
188
 
188
- def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new)
189
+ def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new, refork: false)
189
190
  @project = project
190
191
  @reader = reader
191
192
  @writer = writer
@@ -196,6 +197,8 @@ module Steep
196
197
  @commandline_args = []
197
198
  @job_queue = queue
198
199
  @write_queue = SizedQueue.new(100)
200
+ @refork_mutex = Mutex.new
201
+ @need_to_refork = refork
199
202
 
200
203
  @controller = TypeCheckController.new(project: project)
201
204
  @result_controller = ResultController.new()
@@ -244,8 +247,10 @@ module Steep
244
247
  Steep.logger.info { "Processing SendMessageJob: dest=client, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
245
248
  writer.write job.message
246
249
  when WorkerProcess
247
- Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
248
- job.dest << job.message
250
+ refork_mutex.synchronize do
251
+ Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
252
+ job.dest << job.message
253
+ end
249
254
  end
250
255
  end
251
256
  end
@@ -287,11 +292,13 @@ module Steep
287
292
  end
288
293
  end
289
294
 
290
- waiter = ThreadWaiter.new
291
- each_worker do |worker|
292
- waiter << worker.wait_thread
295
+ waiter = ThreadWaiter.new(each_worker.to_a) {|worker| worker.wait_thread }
296
+ # @type var th: Thread & WorkerProcess::_ProcessWaitThread
297
+ while th = _ = waiter.wait_one()
298
+ if each_worker.any? { |worker| worker.pid == th.pid }
299
+ break # The worker unexpectedly exited
300
+ end
293
301
  end
294
- waiter.wait_one()
295
302
 
296
303
  unless job_queue.closed?
297
304
  # Exit by error
@@ -794,6 +801,76 @@ module Steep
794
801
  if current.finished?
795
802
  finish_type_check(current)
796
803
  @current_type_check_request = nil
804
+ refork_workers
805
+ end
806
+ end
807
+ end
808
+ end
809
+
810
+ def refork_workers
811
+ return unless @need_to_refork
812
+ @need_to_refork = false
813
+
814
+ Thread.new do
815
+ Thread.current.abort_on_exception = true
816
+
817
+ primary, *others = typecheck_workers
818
+ primary or raise
819
+ others.each do |worker|
820
+ worker.index or raise
821
+
822
+ refork_mutex.synchronize do
823
+ refork_finished = Thread::Queue.new
824
+ stdin_in, stdin_out = IO.pipe
825
+ stdout_in, stdout_out = IO.pipe
826
+
827
+ result_controller << send_refork_request(params: { index: worker.index, max_index: typecheck_workers.size }, worker: primary) do |handler|
828
+ handler.on_completion do |response|
829
+ writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdin_out)
830
+ reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdout_in)
831
+
832
+ pid = response[:result][:pid]
833
+ # It does not need to wait worker process
834
+ # because the primary worker monitors it instead.
835
+ #
836
+ # @type var wait_thread: Thread & WorkerProcess::_ProcessWaitThread
837
+ wait_thread = _ = Thread.new { sleep }
838
+ wait_thread.define_singleton_method(:pid) { pid }
839
+
840
+ new_worker = WorkerProcess.new(reader:, writer:, stderr: nil, wait_thread:, name: "#{worker.name}-2", index: worker.index)
841
+ old_worker = typecheck_workers[worker.index] or raise
842
+
843
+ typecheck_workers[(new_worker.index or raise)] = new_worker
844
+
845
+ original_old_worker = old_worker.dup
846
+ old_worker.redirect_to new_worker
847
+
848
+ refork_finished << true
849
+
850
+ result_controller << send_request(method: 'shutdown', worker: original_old_worker) do |handler|
851
+ handler.on_completion do
852
+ send_request(method: 'exit', worker: original_old_worker)
853
+ end
854
+ end
855
+
856
+ Thread.new do
857
+ tags = Steep.logger.formatter.current_tags.dup
858
+ Steep.logger.formatter.push_tags(*tags, "from-worker@#{new_worker.name}")
859
+ new_worker.reader.read do |message|
860
+ job_queue << ReceiveMessageJob.new(source: new_worker, message: message)
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ # The primary worker starts forking when it receives the IOs.
867
+ primary.io_socket or raise
868
+ primary.io_socket.send_io(stdin_in)
869
+ primary.io_socket.send_io(stdout_out)
870
+ stdin_in.close
871
+ stdout_out.close
872
+
873
+ refork_finished.pop
797
874
  end
798
875
  end
799
876
  end
@@ -826,6 +903,25 @@ module Steep
826
903
  end
827
904
  end
828
905
 
906
+ def send_refork_request(id: fresh_request_id(), params:, worker:, &block)
907
+ method = CustomMethods::Refork::METHOD
908
+ Steep.logger.info "Sending request #{method}(#{id}) to #{worker.name}"
909
+
910
+ # @type var message: lsp_request
911
+ message = { method: method, id: id, params: params }
912
+ ResultHandler.new(request: message).tap do |handler|
913
+ yield handler if block
914
+
915
+ job = SendMessageJob.to_worker(worker, message: message)
916
+ case job.dest
917
+ when WorkerProcess
918
+ job.dest << job.message
919
+ else
920
+ raise "Unexpected destination: #{job.dest}"
921
+ end
922
+ end
923
+ end
924
+
829
925
  def group_request()
830
926
  GroupHandler.new().tap do |group|
831
927
  yield group
@@ -51,15 +51,28 @@ module Steep
51
51
 
52
52
  include ChangeBuffer
53
53
 
54
- def initialize(project:, reader:, writer:, assignment:, commandline_args:)
54
+ attr_reader :io_socket
55
+
56
+ def initialize(project:, reader:, writer:, assignment:, commandline_args:, io_socket: nil, buffered_changes: nil, service: nil)
55
57
  super(project: project, reader: reader, writer: writer)
56
58
 
57
59
  @assignment = assignment
58
- @buffered_changes = {}
60
+ @buffered_changes = buffered_changes || {}
59
61
  @mutex = Mutex.new()
60
62
  @queue = Queue.new
61
63
  @commandline_args = commandline_args
62
64
  @current_type_check_guid = nil
65
+ @io_socket = io_socket
66
+ @service = service if service
67
+ @child_pids = []
68
+
69
+ if io_socket
70
+ Signal.trap "SIGCHLD" do
71
+ while pid = Process.wait(-1, Process::WNOHANG)
72
+ raise "Unexpected worker process exit: #{pid}" if @child_pids.include?(pid)
73
+ end
74
+ end
75
+ end
63
76
  end
64
77
 
65
78
  def service
@@ -98,6 +111,39 @@ module Steep
98
111
  queue << GotoJob.implementation(id: request[:id], params: request[:params])
99
112
  when "textDocument/typeDefinition"
100
113
  queue << GotoJob.type_definition(id: request[:id], params: request[:params])
114
+ when CustomMethods::Refork::METHOD
115
+ io_socket or raise
116
+
117
+ # Receive IOs before fork to avoid receiving them from multiple processes
118
+ stdin = io_socket.recv_io
119
+ stdout = io_socket.recv_io
120
+
121
+ if pid = fork
122
+ stdin.close
123
+ stdout.close
124
+ @child_pids << pid
125
+ writer.write(CustomMethods::Refork.response(request[:id], { pid: }))
126
+ else
127
+ io_socket.close
128
+
129
+ reader.close
130
+ writer.close
131
+
132
+ reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdin)
133
+ writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdout)
134
+ Steep.logger.info("Reforked worker: #{Process.pid}, params: #{request[:params]}")
135
+ index = request[:params][:index]
136
+ assignment = Services::PathAssignment.new(max_index: request[:params][:max_index], index: index)
137
+
138
+ worker = self.class.new(project: project, reader: reader, writer: writer, assignment: assignment, commandline_args: commandline_args, io_socket: nil, buffered_changes: buffered_changes, service: service)
139
+
140
+ tags = Steep.logger.formatter.current_tags.dup
141
+ tags[tags.find_index("typecheck:typecheck@0")] = "typecheck:typecheck@#{index}-reforked"
142
+ Steep.logger.formatter.push_tags(tags)
143
+ worker.run()
144
+
145
+ raise "unreachable"
146
+ end
101
147
  end
102
148
  end
103
149