steep 0.39.0 → 0.40.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/CHANGELOG.md +6 -0
  4. data/Rakefile +5 -2
  5. data/bin/output_rebaseline.rb +49 -0
  6. data/bin/output_test.rb +93 -0
  7. data/lib/steep.rb +8 -3
  8. data/lib/steep/cli.rb +1 -1
  9. data/lib/steep/diagnostic/helper.rb +17 -0
  10. data/lib/steep/diagnostic/lsp_formatter.rb +16 -0
  11. data/lib/steep/diagnostic/ruby.rb +623 -0
  12. data/lib/steep/diagnostic/signature.rb +224 -0
  13. data/lib/steep/drivers/annotations.rb +13 -6
  14. data/lib/steep/drivers/check.rb +83 -60
  15. data/lib/steep/drivers/diagnostic_printer.rb +94 -0
  16. data/lib/steep/drivers/stats.rb +125 -29
  17. data/lib/steep/drivers/trace_printer.rb +5 -1
  18. data/lib/steep/drivers/validate.rb +13 -6
  19. data/lib/steep/drivers/watch.rb +26 -9
  20. data/lib/steep/drivers/worker.rb +5 -0
  21. data/lib/steep/project/options.rb +4 -4
  22. data/lib/steep/project/signature_file.rb +8 -2
  23. data/lib/steep/project/stats_calculator.rb +80 -0
  24. data/lib/steep/project/target.rb +64 -53
  25. data/lib/steep/range_extension.rb +29 -0
  26. data/lib/steep/server/base_worker.rb +42 -4
  27. data/lib/steep/server/code_worker.rb +37 -24
  28. data/lib/steep/server/interaction_worker.rb +1 -0
  29. data/lib/steep/server/master.rb +268 -82
  30. data/lib/steep/server/signature_worker.rb +7 -59
  31. data/lib/steep/server/worker_process.rb +9 -9
  32. data/lib/steep/signature/validator.rb +33 -9
  33. data/lib/steep/type_construction.rb +276 -194
  34. data/lib/steep/version.rb +1 -1
  35. data/smoke/alias/a.rb +0 -3
  36. data/smoke/alias/b.rb +0 -1
  37. data/smoke/alias/c.rb +0 -2
  38. data/smoke/alias/test.yaml +73 -0
  39. data/smoke/and/a.rb +0 -3
  40. data/smoke/and/test.yaml +24 -0
  41. data/smoke/array/a.rb +0 -3
  42. data/smoke/array/b.rb +0 -2
  43. data/smoke/array/c.rb +0 -1
  44. data/smoke/array/test.yaml +80 -0
  45. data/smoke/block/a.rb +0 -2
  46. data/smoke/block/b.rb +0 -2
  47. data/smoke/block/d.rb +0 -4
  48. data/smoke/block/test.yaml +96 -0
  49. data/smoke/broken/Steepfile +5 -0
  50. data/smoke/broken/broken.rb +0 -0
  51. data/smoke/broken/broken.rbs +0 -0
  52. data/smoke/broken/test.yaml +6 -0
  53. data/smoke/case/a.rb +0 -3
  54. data/smoke/case/test.yaml +36 -0
  55. data/smoke/class/a.rb +0 -3
  56. data/smoke/class/c.rb +0 -1
  57. data/smoke/class/f.rb +0 -1
  58. data/smoke/class/g.rb +0 -2
  59. data/smoke/class/i.rb +0 -2
  60. data/smoke/class/test.yaml +89 -0
  61. data/smoke/const/a.rb +0 -3
  62. data/smoke/const/b.rb +7 -0
  63. data/smoke/const/b.rbs +5 -0
  64. data/smoke/const/test.yaml +96 -0
  65. data/smoke/diagnostics-rbs-duplicated/Steepfile +5 -0
  66. data/smoke/diagnostics-rbs-duplicated/a.rbs +5 -0
  67. data/smoke/diagnostics-rbs-duplicated/test.yaml +10 -0
  68. data/smoke/diagnostics-rbs/Steepfile +5 -0
  69. data/smoke/diagnostics-rbs/duplicated-method-definition.rbs +20 -0
  70. data/smoke/diagnostics-rbs/generic-parameter-mismatch.rbs +7 -0
  71. data/smoke/diagnostics-rbs/invalid-method-overload.rbs +3 -0
  72. data/smoke/diagnostics-rbs/invalid-type-application.rbs +7 -0
  73. data/smoke/diagnostics-rbs/invalid_variance_annotation.rbs +3 -0
  74. data/smoke/diagnostics-rbs/recursive-alias.rbs +5 -0
  75. data/smoke/diagnostics-rbs/recursive-class.rbs +8 -0
  76. data/smoke/diagnostics-rbs/superclass-mismatch.rbs +7 -0
  77. data/smoke/diagnostics-rbs/test.yaml +142 -0
  78. data/smoke/diagnostics-rbs/unknown-method-alias.rbs +3 -0
  79. data/smoke/diagnostics-rbs/unknown-type-name.rbs +13 -0
  80. data/smoke/diagnostics/Steepfile +5 -0
  81. data/smoke/diagnostics/a.rbs +26 -0
  82. data/smoke/diagnostics/argument_type_mismatch.rb +1 -0
  83. data/smoke/diagnostics/block_body_type_mismatch.rb +1 -0
  84. data/smoke/diagnostics/block_type_mismatch.rb +3 -0
  85. data/smoke/diagnostics/break_type_mismatch.rb +1 -0
  86. data/smoke/diagnostics/else_on_exhaustive_case.rb +12 -0
  87. data/smoke/diagnostics/incompatible_annotation.rb +6 -0
  88. data/smoke/diagnostics/incompatible_argument.rb +1 -0
  89. data/smoke/diagnostics/incompatible_assignment.rb +8 -0
  90. data/smoke/diagnostics/method_arity_mismatch.rb +11 -0
  91. data/smoke/diagnostics/method_body_type_mismatch.rb +6 -0
  92. data/smoke/diagnostics/method_definition_missing.rb +2 -0
  93. data/smoke/diagnostics/method_return_type_annotation_mismatch.rb +7 -0
  94. data/smoke/diagnostics/missing_keyword.rb +1 -0
  95. data/smoke/diagnostics/no_method.rb +1 -0
  96. data/smoke/diagnostics/required_block_missing.rb +1 -0
  97. data/smoke/diagnostics/return_type_mismatch.rb +6 -0
  98. data/smoke/diagnostics/test.yaml +333 -0
  99. data/smoke/diagnostics/unexpected_block_given.rb +1 -0
  100. data/smoke/diagnostics/unexpected_dynamic_method.rb +3 -0
  101. data/smoke/diagnostics/unexpected_jump.rb +4 -0
  102. data/smoke/diagnostics/unexpected_jump_value.rb +3 -0
  103. data/smoke/diagnostics/unexpected_keyword.rb +1 -0
  104. data/smoke/diagnostics/unexpected_splat.rb +1 -0
  105. data/smoke/diagnostics/unexpected_yield.rb +6 -0
  106. data/smoke/diagnostics/unknown_constant_assigned.rb +7 -0
  107. data/smoke/diagnostics/unresolved_overloading.rb +1 -0
  108. data/smoke/diagnostics/unsatisfiable_constraint.rb +7 -0
  109. data/smoke/diagnostics/unsupported_syntax.rb +2 -0
  110. data/smoke/dstr/a.rb +0 -1
  111. data/smoke/dstr/test.yaml +10 -0
  112. data/smoke/ensure/a.rb +0 -4
  113. data/smoke/ensure/test.yaml +47 -0
  114. data/smoke/enumerator/a.rb +0 -6
  115. data/smoke/enumerator/b.rb +0 -3
  116. data/smoke/enumerator/test.yaml +100 -0
  117. data/smoke/extension/a.rb +0 -1
  118. data/smoke/extension/b.rb +0 -2
  119. data/smoke/extension/c.rb +0 -1
  120. data/smoke/extension/test.yaml +50 -0
  121. data/smoke/hash/b.rb +0 -1
  122. data/smoke/hash/c.rb +0 -3
  123. data/smoke/hash/d.rb +0 -1
  124. data/smoke/hash/e.rb +0 -1
  125. data/smoke/hash/test.yaml +62 -0
  126. data/smoke/hello/hello.rb +0 -2
  127. data/smoke/hello/test.yaml +18 -0
  128. data/smoke/if/a.rb +0 -2
  129. data/smoke/if/test.yaml +27 -0
  130. data/smoke/implements/a.rb +0 -2
  131. data/smoke/implements/test.yaml +16 -0
  132. data/smoke/initialize/test.yaml +4 -0
  133. data/smoke/integer/a.rb +0 -7
  134. data/smoke/integer/test.yaml +66 -0
  135. data/smoke/interface/a.rb +0 -2
  136. data/smoke/interface/test.yaml +16 -0
  137. data/smoke/kwbegin/a.rb +0 -1
  138. data/smoke/kwbegin/test.yaml +14 -0
  139. data/smoke/lambda/a.rb +1 -4
  140. data/smoke/lambda/test.yaml +28 -0
  141. data/smoke/literal/a.rb +0 -5
  142. data/smoke/literal/b.rb +0 -2
  143. data/smoke/literal/test.yaml +79 -0
  144. data/smoke/map/test.yaml +4 -0
  145. data/smoke/method/a.rb +0 -5
  146. data/smoke/method/b.rb +0 -1
  147. data/smoke/method/test.yaml +71 -0
  148. data/smoke/module/a.rb +0 -2
  149. data/smoke/module/b.rb +0 -2
  150. data/smoke/module/c.rb +0 -1
  151. data/smoke/module/d.rb +0 -1
  152. data/smoke/module/f.rb +0 -2
  153. data/smoke/module/test.yaml +51 -0
  154. data/smoke/regexp/a.rb +0 -38
  155. data/smoke/regexp/b.rb +0 -26
  156. data/smoke/regexp/test.yaml +372 -0
  157. data/smoke/regression/set_divide.rb +0 -4
  158. data/smoke/regression/test.yaml +38 -0
  159. data/smoke/rescue/a.rb +0 -5
  160. data/smoke/rescue/test.yaml +60 -0
  161. data/smoke/self/a.rb +0 -2
  162. data/smoke/self/test.yaml +16 -0
  163. data/smoke/skip/skip.rb +0 -2
  164. data/smoke/skip/test.yaml +16 -0
  165. data/smoke/stdout/test.yaml +4 -0
  166. data/smoke/super/a.rb +0 -4
  167. data/smoke/super/test.yaml +52 -0
  168. data/smoke/toplevel/a.rb +0 -1
  169. data/smoke/toplevel/test.yaml +12 -0
  170. data/smoke/tsort/a.rb +0 -3
  171. data/smoke/tsort/test.yaml +32 -0
  172. data/smoke/type_case/a.rb +0 -4
  173. data/smoke/type_case/test.yaml +33 -0
  174. data/smoke/yield/a.rb +0 -3
  175. data/smoke/yield/b.rb +6 -0
  176. data/smoke/yield/test.yaml +49 -0
  177. data/steep.gemspec +3 -3
  178. metadata +108 -17
  179. data/bin/smoke_runner.rb +0 -139
  180. data/lib/steep/drivers/signature_error_printer.rb +0 -25
  181. data/lib/steep/errors.rb +0 -594
  182. data/lib/steep/signature/errors.rb +0 -128
  183. data/lib/steep/type_assignability.rb +0 -367
@@ -0,0 +1,29 @@
1
+ class RBS::Location
2
+ def as_lsp_range
3
+ {
4
+ start: {
5
+ line: start_line - 1,
6
+ character: start_column
7
+ },
8
+ end: {
9
+ line: end_line - 1,
10
+ character: end_column
11
+ }
12
+ }
13
+ end
14
+ end
15
+
16
+ class Parser::Source::Range
17
+ def as_lsp_range
18
+ {
19
+ start: {
20
+ line: line - 1,
21
+ character: column
22
+ },
23
+ end: {
24
+ line: last_line - 1,
25
+ character: last_column
26
+ }
27
+ }
28
+ end
29
+ end
@@ -8,13 +8,28 @@ module Steep
8
8
  attr_reader :project
9
9
  attr_reader :reader, :writer
10
10
 
11
+ ShutdownJob = Struct.new(:id, keyword_init: true)
12
+
11
13
  def initialize(project:, reader:, writer:)
12
14
  @project = project
13
15
  @reader = reader
14
16
  @writer = writer
17
+ @skip_job = false
15
18
  @shutdown = false
16
19
  end
17
20
 
21
+ def skip_jobs_after_shutdown!(flag = true)
22
+ @skip_jobs_after_shutdown = flag
23
+ end
24
+
25
+ def skip_jobs_after_shutdown?
26
+ @skip_jobs_after_shutdown
27
+ end
28
+
29
+ def skip_job?
30
+ @skip_job
31
+ end
32
+
18
33
  def handle_request(request)
19
34
  # process request
20
35
  end
@@ -29,7 +44,29 @@ module Steep
29
44
  Steep.logger.formatter.push_tags(*tags)
30
45
  Steep.logger.tagged "background" do
31
46
  while job = queue.pop
32
- handle_job(job) unless @shutdown
47
+ case job
48
+ when ShutdownJob
49
+ writer.write(id: job.id, result: nil)
50
+ else
51
+ if skip_job?
52
+ Steep.logger.info "Skipping job..."
53
+ else
54
+ begin
55
+ handle_job(job)
56
+ rescue => exn
57
+ Steep.log_error exn
58
+ writer.write(
59
+ {
60
+ method: "window/showMessage",
61
+ params: {
62
+ type: LSP::Constant::MessageType::ERROR,
63
+ message: "Unexpected error: #{exn.message} (#{exn.class})"
64
+ }
65
+ }
66
+ )
67
+ end
68
+ end
69
+ end
33
70
  end
34
71
  end
35
72
  end
@@ -37,10 +74,12 @@ module Steep
37
74
  Steep.logger.tagged "frontend" do
38
75
  begin
39
76
  reader.read do |request|
77
+ Steep.logger.info "Received message from master: #{request[:method]}(#{request[:id]})"
40
78
  case request[:method]
41
79
  when "shutdown"
42
- @shutdown = true
43
- writer.write(id: request[:id], result: nil)
80
+ queue << ShutdownJob.new(id: request[:id])
81
+ @skip_job = skip_jobs_after_shutdown?
82
+ queue.close
44
83
  when "exit"
45
84
  break
46
85
  else
@@ -48,7 +87,6 @@ module Steep
48
87
  end
49
88
  end
50
89
  ensure
51
- queue << nil
52
90
  thread.join
53
91
  end
54
92
  end
@@ -3,6 +3,9 @@ module Steep
3
3
  class CodeWorker < BaseWorker
4
4
  LSP = LanguageServer::Protocol
5
5
 
6
+ TypeCheckJob = Struct.new(:target, :path, keyword_init: true)
7
+ StatsJob = Struct.new(:request, :paths, keyword_init: true)
8
+
6
9
  include Utils
7
10
 
8
11
  attr_reader :typecheck_paths
@@ -17,7 +20,7 @@ module Steep
17
20
 
18
21
  def enqueue_type_check(target:, path:)
19
22
  Steep.logger.info "Enqueueing type check: #{target.name}::#{path}..."
20
- queue << [target, path]
23
+ queue << TypeCheckJob.new(target: target, path: path)
21
24
  end
22
25
 
23
26
  def typecheck_file(path, target)
@@ -43,6 +46,23 @@ module Steep
43
46
  )
44
47
  end
45
48
 
49
+ def calculate_stats(request_id, paths)
50
+ calculator = Project::StatsCalculator.new(project: project)
51
+
52
+ stats = paths.map do |path|
53
+ if typecheck_paths.include?(path)
54
+ if target = project.target_for_source_path(path)
55
+ calculator.calc_stats(target, path)
56
+ end
57
+ end
58
+ end.compact
59
+
60
+ writer.write(
61
+ id: request_id,
62
+ result: stats.map(&:as_json)
63
+ )
64
+ end
65
+
46
66
  def source_diagnostics(source, options)
47
67
  case status = source.status
48
68
  when Project::SourceFile::ParseErrorStatus
@@ -65,24 +85,8 @@ module Steep
65
85
  )
66
86
  ]
67
87
  when Project::SourceFile::TypeCheckStatus
68
- status.typing.errors.select {|error| options.error_to_report?(error) }.map do |error|
69
- loc = error.location_to_str
70
-
71
- LSP::Interface::Diagnostic.new(
72
- message: StringIO.new.tap {|io| error.print_to(io) }.string.gsub(/\A#{Regexp.escape(loc)}: /, "").chomp,
73
- severity: LSP::Constant::DiagnosticSeverity::ERROR,
74
- range: LSP::Interface::Range.new(
75
- start: LSP::Interface::Position.new(
76
- line: error.node.loc.line - 1,
77
- character: error.node.loc.column
78
- ),
79
- end: LSP::Interface::Position.new(
80
- line: error.node.loc.last_line - 1,
81
- character: error.node.loc.last_column
82
- )
83
- )
84
- )
85
- end
88
+ formatter = Diagnostic::LSPFormatter.new()
89
+ status.typing.errors.select {|error| options.error_to_report?(error) }.map {|error| formatter.format(error) }
86
90
  when Project::SourceFile::TypeCheckErrorStatus
87
91
  []
88
92
  end
@@ -91,7 +95,6 @@ module Steep
91
95
  def handle_request(request)
92
96
  case request[:method]
93
97
  when "initialize"
94
- # Don't respond to initialize request, but start type checking.
95
98
  project.targets.each do |target|
96
99
  target.source_files.each_key do |path|
97
100
  if typecheck_paths.include?(path)
@@ -100,10 +103,17 @@ module Steep
100
103
  end
101
104
  end
102
105
 
106
+ writer.write({ id: request[:id], result: nil })
107
+
103
108
  when "workspace/executeCommand"
104
- if request[:params][:command] == "steep/registerSourceToWorker"
109
+ Steep.logger.info { "Executing command: #{request[:params][:command]}, arguments=#{request[:params][:arguments].map(&:inspect).join(", ")}" }
110
+ case request[:params][:command]
111
+ when "steep/registerSourceToWorker"
105
112
  paths = request[:params][:arguments].map {|arg| source_path(URI.parse(arg)) }
106
113
  typecheck_paths.merge(paths)
114
+ when "steep/stats"
115
+ paths = request[:params][:arguments].map {|arg| source_path(URI.parse(arg)) }
116
+ queue << StatsJob.new(paths: paths, request: request)
107
117
  end
108
118
 
109
119
  when "textDocument/didChange"
@@ -128,9 +138,12 @@ module Steep
128
138
  end
129
139
 
130
140
  def handle_job(job)
131
- target, path = job
132
-
133
- typecheck_file(path, target)
141
+ case job
142
+ when TypeCheckJob
143
+ typecheck_file(job.path, job.target)
144
+ when StatsJob
145
+ calculate_stats(job.request[:id], job.paths)
146
+ end
134
147
  end
135
148
  end
136
149
  end
@@ -17,6 +17,7 @@ module Steep
17
17
  case request[:method]
18
18
  when "initialize"
19
19
  # nop
20
+ writer.write({ id: request[:id], result: nil })
20
21
 
21
22
  when "textDocument/didChange"
22
23
  update_source(request)
@@ -6,7 +6,6 @@ module Steep
6
6
  attr_reader :steepfile
7
7
  attr_reader :project
8
8
  attr_reader :reader, :writer
9
- attr_reader :queue
10
9
  attr_reader :worker_count
11
10
  attr_reader :worker_to_paths
12
11
 
@@ -14,75 +13,179 @@ module Steep
14
13
  attr_reader :signature_worker
15
14
  attr_reader :code_workers
16
15
 
16
+ attr_reader :response_handlers
17
+
18
+ # There are four types of threads:
19
+ #
20
+ # 1. Main thread -- Reads messages from client
21
+ # 2. Worker threads -- Reads messages from associated worker
22
+ # 3. Reconciliation thread -- Receives message from worker threads, reconciles, processes, and forwards to write thread
23
+ # 4. Write thread -- Writes messages to client
24
+ #
25
+ # We have two queues:
26
+ #
27
+ # 1. `recon_queue` is to pass messages from worker threads to reconciliation thread
28
+ # 2. `write` thread is to pass messages to write thread
29
+ #
30
+ # Message passing: Client -> Server (Master) -> Worker
31
+ #
32
+ # 1. Client -> Server
33
+ # Master receives messages from the LSP client on main thread.
34
+ #
35
+ # 2. Master -> Worker
36
+ # Master writes messages to workers on main thread.
37
+ #
38
+ # Message passing: Worker -> Server (Master) -> (reconciliation queue) -> (write queue) -> Client
39
+ #
40
+ # 3. Worker -> Master
41
+ # Master receives messages on threads dedicated for each worker.
42
+ # The messages sent from workers are then forwarded to the reconciliation thread through reconciliation queue.
43
+ #
44
+ # 4. Server -> Client
45
+ # The reconciliation thread reads messages from reconciliation queue, does something, and finally sends messages to the client via write queue.
46
+ #
47
+ attr_reader :write_queue
48
+ attr_reader :recon_queue
49
+
17
50
  include Utils
18
51
 
52
+ class ResponseHandler
53
+ attr_reader :workers
54
+
55
+ attr_reader :request
56
+ attr_reader :responses
57
+
58
+ attr_reader :on_response_handlers
59
+ attr_reader :on_completion_handlers
60
+
61
+ def initialize(request:, workers:)
62
+ @workers = []
63
+
64
+ @request = request
65
+ @responses = workers.each.with_object({}) do |worker, hash|
66
+ hash[worker] = nil
67
+ end
68
+
69
+ @on_response_handlers = []
70
+ @on_completion_handlers = []
71
+ end
72
+
73
+ def on_response(&block)
74
+ on_response_handlers << block
75
+ end
76
+
77
+ def on_completion(&block)
78
+ on_completion_handlers << block
79
+ end
80
+
81
+ def request_id
82
+ request[:id]
83
+ end
84
+
85
+ def process_response(response, worker)
86
+ responses[worker] = response
87
+
88
+ on_response_handlers.each do |handler|
89
+ handler[worker, response]
90
+ end
91
+
92
+ if completed?
93
+ on_completion_handlers.each do |handler|
94
+ handler[*responses.values]
95
+ end
96
+ end
97
+ end
98
+
99
+ def completed?
100
+ responses.each_value.none?(&:nil?)
101
+ end
102
+ end
103
+
19
104
  def initialize(project:, reader:, writer:, interaction_worker:, signature_worker:, code_workers:, queue: Queue.new)
20
105
  @project = project
21
106
  @reader = reader
22
107
  @writer = writer
23
- @queue = queue
108
+ @write_queue = queue
109
+ @recon_queue = Queue.new
24
110
  @interaction_worker = interaction_worker
25
111
  @signature_worker = signature_worker
26
112
  @code_workers = code_workers
27
113
  @worker_to_paths = {}
28
114
  @shutdown_request_id = nil
115
+ @response_handlers = {}
29
116
  end
30
117
 
31
118
  def start
32
- source_paths = project.all_source_files
33
- bin_size = (source_paths.size / code_workers.size) + 1
34
- source_paths.each_slice(bin_size).with_index do |paths, index|
35
- register_code_to_worker(paths, worker: code_workers[index])
36
- end
119
+ Steep.logger.tagged "master" do
120
+ tags = Steep.logger.formatter.current_tags.dup
121
+
122
+ Steep.logger.info "Registering all code to workers..."
123
+ source_paths = project.all_source_files
124
+ bin_size = (source_paths.size / code_workers.size) + 1
125
+ source_paths.each_slice(bin_size).with_index do |paths, index|
126
+ register_code_to_worker(paths, worker: code_workers[index])
127
+ end
37
128
 
38
- tags = Steep.logger.formatter.current_tags.dup
39
- tags << "master"
129
+ worker_threads = []
40
130
 
41
- Thread.new do
42
- Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
43
- interaction_worker.reader.read do |message|
44
- process_message_from_worker(message)
131
+ worker_threads << Thread.new do
132
+ Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
133
+ interaction_worker.reader.read do |message|
134
+ process_message_from_worker(message, worker: interaction_worker)
135
+ end
45
136
  end
46
- end
47
137
 
48
- Thread.new do
49
- Steep.logger.formatter.push_tags(*tags, "from-worker@signature")
50
- signature_worker.reader.read do |message|
51
- process_message_from_worker(message)
138
+ worker_threads << Thread.new do
139
+ Steep.logger.formatter.push_tags(*tags, "from-worker@signature")
140
+ signature_worker.reader.read do |message|
141
+ process_message_from_worker(message, worker: signature_worker)
142
+ end
52
143
  end
53
- end
54
144
 
55
- code_workers.each do |worker|
56
- Thread.new do
57
- Steep.logger.formatter.push_tags(*tags, "from-worker@#{worker.name}")
58
- worker.reader.read do |message|
59
- process_message_from_worker(message)
145
+ code_workers.each do |worker|
146
+ worker_threads << Thread.new do
147
+ Steep.logger.formatter.push_tags(*tags, "from-worker@#{worker.name}")
148
+ worker.reader.read do |message|
149
+ process_message_from_worker(message, worker: worker)
150
+ end
60
151
  end
61
152
  end
62
- end
63
153
 
64
- Thread.new do
65
- Steep.logger.formatter.push_tags(*tags, "from-client")
66
- reader.read do |request|
67
- process_message_from_client(request)
154
+ worker_threads << Thread.new do
155
+ Steep.logger.formatter.push_tags(*tags, "write")
156
+ while message = write_queue.pop
157
+ writer.write(message)
158
+ end
159
+
160
+ writer.io.close
68
161
  end
69
- end
70
162
 
71
- while job = queue.pop
72
- if @shutdown_request_id
73
- if job[:id] == @shutdown_request_id
74
- writer.write(job)
75
- break
163
+ worker_threads << Thread.new do
164
+ Steep.logger.formatter.push_tags(*tags, "reconciliation")
165
+ while (message, worker = recon_queue.pop)
166
+ id = message[:id]
167
+ handler = response_handlers[id] or raise
168
+
169
+ Steep.logger.info "Processing response to #{handler.request[:method]}(#{id}) from #{worker.name}"
170
+
171
+ handler.process_response(message, worker)
172
+
173
+ if handler.completed?
174
+ Steep.logger.info "Response to #{handler.request[:method]}(#{id}) completed"
175
+ response_handlers.delete(id)
176
+ end
76
177
  end
77
- else
78
- writer.write(job)
79
178
  end
80
- end
81
179
 
82
- writer.io.close
180
+ Steep.logger.tagged "main" do
181
+ reader.read do |request|
182
+ process_message_from_client(request) or break
183
+ end
83
184
 
84
- each_worker do |w|
85
- w.shutdown()
185
+ worker_threads.each do |thread|
186
+ thread.join
187
+ end
188
+ end
86
189
  end
87
190
  end
88
191
 
@@ -97,28 +200,29 @@ module Steep
97
200
  end
98
201
 
99
202
  def process_message_from_client(message)
203
+ Steep.logger.info "Received message #{message[:method]}(#{message[:id]})"
100
204
  id = message[:id]
101
205
 
102
206
  case message[:method]
103
207
  when "initialize"
104
- queue << {
105
- id: id,
106
- result: LSP::Interface::InitializeResult.new(
107
- capabilities: LSP::Interface::ServerCapabilities.new(
108
- text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
109
- change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL
110
- ),
111
- hover_provider: true,
112
- completion_provider: LSP::Interface::CompletionOptions.new(
113
- trigger_characters: [".", "@"]
114
- ),
115
- workspace_symbol_provider: true
116
- )
117
- )
118
- }
119
-
120
- each_worker do |worker|
121
- worker << message
208
+ broadcast_request(message) do |handler|
209
+ handler.on_completion do
210
+ write_queue << {
211
+ id: id,
212
+ result: LSP::Interface::InitializeResult.new(
213
+ capabilities: LSP::Interface::ServerCapabilities.new(
214
+ text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
215
+ change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL
216
+ ),
217
+ hover_provider: true,
218
+ completion_provider: LSP::Interface::CompletionOptions.new(
219
+ trigger_characters: [".", "@"]
220
+ ),
221
+ workspace_symbol_provider: true
222
+ )
223
+ )
224
+ }
225
+ end
122
226
  end
123
227
 
124
228
  when "textDocument/didChange"
@@ -131,33 +235,106 @@ module Steep
131
235
  register_code_to_worker [path], worker: least_busy_worker()
132
236
  end
133
237
 
134
- each_worker do |worker|
135
- worker << message
136
- end
137
-
138
- when "textDocument/hover"
139
- interaction_worker << message
238
+ broadcast_notification(message)
140
239
 
141
- when "textDocument/completion"
142
- interaction_worker << message
240
+ when "textDocument/hover", "textDocument/completion"
241
+ send_request(message, worker: interaction_worker) do |handler|
242
+ handler.on_completion do |response|
243
+ write_queue << response
244
+ end
245
+ end
143
246
 
144
247
  when "textDocument/open"
145
248
  # Ignores open notification
146
249
 
147
250
  when "workspace/symbol"
148
- signature_worker << message
251
+ send_request(message, worker: signature_worker) do |handler|
252
+ handler.on_completion do |response|
253
+ write_queue << response
254
+ end
255
+ end
256
+
257
+ when "workspace/executeCommand"
258
+ case message[:params][:command]
259
+ when "steep/stats"
260
+ send_request(message, workers: code_workers) do |handler|
261
+ handler.on_completion do |*responses|
262
+ stats = responses.flat_map {|resp| resp[:result] }
263
+
264
+ write_queue << {
265
+ id: handler.request_id,
266
+ result: stats
267
+ }
268
+ end
269
+ end
270
+ end
149
271
 
150
272
  when "shutdown"
151
- queue << { id: id, result: nil }
152
- @shutdown_request_id = id
273
+ broadcast_request(message) do |handler|
274
+ handler.on_completion do |*_|
275
+ write_queue << { id: id, result: nil}
276
+
277
+ write_queue.close
278
+ recon_queue.close
279
+ end
280
+ end
153
281
 
154
282
  when "exit"
155
- queue << nil
283
+ broadcast_notification(message)
284
+
285
+ return false
156
286
  end
287
+
288
+ true
157
289
  end
158
290
 
159
- def process_message_from_worker(message)
160
- queue << message
291
+ def broadcast_notification(message)
292
+ Steep.logger.info "Broadcasting notification #{message[:method]}"
293
+ each_worker do |worker|
294
+ worker << message
295
+ end
296
+ end
297
+
298
+ def send_notification(message, worker:)
299
+ Steep.logger.info "Sending notification #{message[:method]} to #{worker.name}"
300
+ worker << message
301
+ end
302
+
303
+ def send_request(message, worker: nil, workers: [])
304
+ workers << worker if worker
305
+
306
+ Steep.logger.info "Sending request #{message[:method]}(#{message[:id]}) to #{workers.map(&:name).join(", ")}"
307
+ handler = ResponseHandler.new(request: message, workers: workers)
308
+ yield(handler) if block_given?
309
+ response_handlers[handler.request_id] = handler
310
+
311
+ workers.each do |w|
312
+ w << message
313
+ end
314
+ end
315
+
316
+ def broadcast_request(message)
317
+ Steep.logger.info "Broadcasting request #{message[:method]}(#{message[:id]})"
318
+ handler = ResponseHandler.new(request: message, workers: each_worker.to_a)
319
+ yield(handler) if block_given?
320
+ response_handlers[handler.request_id] = handler
321
+
322
+ each_worker do |worker|
323
+ worker << message
324
+ end
325
+ end
326
+
327
+ def process_message_from_worker(message, worker:)
328
+ case
329
+ when message.key?(:id) && !message.key?(:method)
330
+ # Response from worker
331
+ Steep.logger.info "Received response #{message[:id]} from worker"
332
+ recon_queue << [message, worker]
333
+ when message.key?(:method) && !message.key?(:id)
334
+ # Notification from worker
335
+ Steep.logger.info "Received notification #{message[:method]} from worker"
336
+ write_queue << message
337
+ end
161
338
  end
162
339
 
163
340
  def paths_for(worker)
@@ -177,15 +354,24 @@ module Steep
177
354
  def register_code_to_worker(paths, worker:)
178
355
  paths_for(worker).merge(paths)
179
356
 
180
- worker << {
181
- method: "workspace/executeCommand",
182
- params: LSP::Interface::ExecuteCommandParams.new(
183
- command: "steep/registerSourceToWorker",
184
- arguments: paths.map do |path|
185
- "file://#{project.absolute_path(path)}"
186
- end
187
- )
188
- }
357
+ send_notification(
358
+ {
359
+ method: "workspace/executeCommand",
360
+ params: LSP::Interface::ExecuteCommandParams.new(
361
+ command: "steep/registerSourceToWorker",
362
+ arguments: paths.map do |path|
363
+ "file://#{project.absolute_path(path)}"
364
+ end
365
+ )
366
+ },
367
+ worker: worker
368
+ )
369
+ end
370
+
371
+ def kill
372
+ each_worker do |worker|
373
+ worker.kill
374
+ end
189
375
  end
190
376
  end
191
377
  end