steep 0.42.0 → 0.43.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/steep.rb +4 -3
- data/lib/steep/annotation_parser.rb +10 -2
- data/lib/steep/cli.rb +1 -0
- data/lib/steep/diagnostic/ruby.rb +15 -6
- data/lib/steep/diagnostic/signature.rb +28 -11
- data/lib/steep/drivers/annotations.rb +1 -3
- data/lib/steep/drivers/check.rb +17 -7
- data/lib/steep/drivers/diagnostic_printer.rb +4 -0
- data/lib/steep/drivers/langserver.rb +1 -0
- data/lib/steep/drivers/print_project.rb +1 -1
- data/lib/steep/drivers/stats.rb +125 -105
- data/lib/steep/drivers/utils/driver_helper.rb +35 -0
- data/lib/steep/drivers/validate.rb +1 -1
- data/lib/steep/drivers/watch.rb +12 -10
- data/lib/steep/index/signature_symbol_provider.rb +20 -6
- data/lib/steep/project/target.rb +4 -4
- data/lib/steep/server/interaction_worker.rb +2 -3
- data/lib/steep/server/master.rb +621 -170
- data/lib/steep/server/type_check_worker.rb +127 -13
- data/lib/steep/server/worker_process.rb +7 -4
- data/lib/steep/services/completion_provider.rb +2 -2
- data/lib/steep/services/hover_content.rb +5 -4
- data/lib/steep/services/path_assignment.rb +6 -8
- data/lib/steep/services/signature_service.rb +43 -9
- data/lib/steep/services/type_check_service.rb +184 -138
- data/lib/steep/signature/validator.rb +17 -9
- data/lib/steep/source.rb +21 -18
- data/lib/steep/subtyping/constraints.rb +2 -2
- data/lib/steep/type_construction.rb +223 -125
- data/lib/steep/type_inference/block_params.rb +1 -1
- data/lib/steep/type_inference/context.rb +22 -0
- data/lib/steep/type_inference/logic.rb +1 -1
- data/lib/steep/type_inference/logic_type_interpreter.rb +3 -3
- data/lib/steep/version.rb +1 -1
- data/smoke/implements/b.rb +13 -0
- data/smoke/implements/b.rbs +12 -0
- data/smoke/regression/issue_328.rb +1 -0
- data/smoke/regression/issue_328.rbs +0 -0
- data/smoke/regression/issue_332.rb +11 -0
- data/smoke/regression/issue_332.rbs +19 -0
- data/smoke/regression/masgn.rb +4 -0
- data/smoke/regression/test_expectations.yml +29 -0
- data/smoke/regression/thread.rb +7 -0
- data/smoke/super/test_expectations.yml +2 -12
- data/steep.gemspec +2 -2
- metadata +40 -20
@@ -20,6 +20,41 @@ module Steep
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
def request_id
|
25
|
+
SecureRandom.alphanumeric(10)
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait_for_response_id(reader:, id:, unknown_responses: :ignore)
|
29
|
+
wait_for_message(reader: reader, unknown_messages: unknown_responses) do |response|
|
30
|
+
response[:id] == id
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def shutdown_exit(writer:, reader:)
|
35
|
+
request_id().tap do |id|
|
36
|
+
writer.write({ method: :shutdown, id: id })
|
37
|
+
wait_for_response_id(reader: reader, id: id)
|
38
|
+
end
|
39
|
+
writer.write({ method: :exit })
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_message(reader:, unknown_messages: :ignore, &block)
|
43
|
+
reader.read do |message|
|
44
|
+
if yield(message)
|
45
|
+
return message
|
46
|
+
else
|
47
|
+
case unknown_messages
|
48
|
+
when :ignore
|
49
|
+
# nop
|
50
|
+
when :log
|
51
|
+
Steep.logger.error { "Unexpected message: #{message.inspect}" }
|
52
|
+
when :raise
|
53
|
+
raise "Unexpected message: #{message.inspect}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
23
58
|
end
|
24
59
|
end
|
25
60
|
end
|
@@ -18,7 +18,7 @@ module Steep
|
|
18
18
|
any_error = false
|
19
19
|
|
20
20
|
project.targets.each do |target|
|
21
|
-
controller = Services::SignatureService.load_from(target.new_env_loader)
|
21
|
+
controller = Services::SignatureService.load_from(target.new_env_loader(project: project))
|
22
22
|
|
23
23
|
changes = file_loader.load_changes(target.signature_pattern, changes: {})
|
24
24
|
controller.update(changes)
|
data/lib/steep/drivers/watch.rb
CHANGED
@@ -48,13 +48,17 @@ module Steep
|
|
48
48
|
interaction_worker: nil,
|
49
49
|
typecheck_workers: typecheck_workers
|
50
50
|
)
|
51
|
+
master.typecheck_automatically = false
|
52
|
+
master.commandline_args.push(*dirs)
|
51
53
|
|
52
54
|
main_thread = Thread.start do
|
53
55
|
master.start()
|
54
56
|
end
|
55
57
|
main_thread.abort_on_exception = true
|
56
58
|
|
57
|
-
|
59
|
+
initialize_id = request_id()
|
60
|
+
client_writer.write(method: "initialize", id: initialize_id)
|
61
|
+
wait_for_response_id(reader: client_reader, id: initialize_id)
|
58
62
|
|
59
63
|
Steep.logger.info "Watching #{dirs.join(", ")}..."
|
60
64
|
|
@@ -104,10 +108,15 @@ module Steep
|
|
104
108
|
end
|
105
109
|
end
|
106
110
|
end
|
111
|
+
|
112
|
+
client_writer.write(method: "$/typecheck", params: { guid: nil })
|
107
113
|
end.tap(&:start)
|
108
114
|
|
109
115
|
begin
|
110
116
|
stdout.puts Rainbow("👀 Watching directories, Ctrl-C to stop.").bold
|
117
|
+
|
118
|
+
client_writer.write(method: "$/typecheck", params: { guid: nil })
|
119
|
+
|
111
120
|
client_reader.read do |response|
|
112
121
|
case response[:method]
|
113
122
|
when "textDocument/publishDiagnostics"
|
@@ -121,6 +130,7 @@ module Steep
|
|
121
130
|
unless diagnostics.empty?
|
122
131
|
diagnostics.each do |diagnostic|
|
123
132
|
printer.print(diagnostic)
|
133
|
+
stdout.flush
|
124
134
|
end
|
125
135
|
end
|
126
136
|
when "window/showMessage"
|
@@ -132,16 +142,8 @@ module Steep
|
|
132
142
|
end
|
133
143
|
end
|
134
144
|
rescue Interrupt
|
135
|
-
shutdown_id = -1
|
136
145
|
stdout.puts "Shutting down workers..."
|
137
|
-
|
138
|
-
client_reader.read do |response|
|
139
|
-
if response[:id] == shutdown_id
|
140
|
-
break
|
141
|
-
end
|
142
|
-
end
|
143
|
-
client_writer.write({ method: :exit })
|
144
|
-
client_writer.io.close()
|
146
|
+
shutdown_exit(reader: client_reader, writer: client_writer)
|
145
147
|
end
|
146
148
|
|
147
149
|
listener.stop
|
@@ -4,10 +4,14 @@ module Steep
|
|
4
4
|
LSP = LanguageServer::Protocol
|
5
5
|
SymbolInformation = Struct.new(:name, :kind, :container_name, :location, keyword_init: true)
|
6
6
|
|
7
|
+
attr_reader :project
|
7
8
|
attr_reader :indexes
|
9
|
+
attr_reader :assignment
|
8
10
|
|
9
|
-
def initialize()
|
11
|
+
def initialize(project:, assignment:)
|
10
12
|
@indexes = []
|
13
|
+
@project = project
|
14
|
+
@assignment = assignment
|
11
15
|
end
|
12
16
|
|
13
17
|
def self.test_type_name(query, type_name)
|
@@ -33,7 +37,17 @@ module Steep
|
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
36
|
-
def
|
40
|
+
def assigned?(path)
|
41
|
+
if path.relative?
|
42
|
+
if project.targets.any? {|target| target.possible_signature_file?(path) }
|
43
|
+
path = project.absolute_path(path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
assignment =~ path
|
48
|
+
end
|
49
|
+
|
50
|
+
def query_symbol(query)
|
37
51
|
symbols = []
|
38
52
|
|
39
53
|
indexes.each do |index|
|
@@ -46,7 +60,7 @@ module Steep
|
|
46
60
|
name = entry.type_name.name.to_s
|
47
61
|
|
48
62
|
entry.declarations.each do |decl|
|
49
|
-
next unless
|
63
|
+
next unless assigned?(Pathname(decl.location.buffer.name))
|
50
64
|
|
51
65
|
case decl
|
52
66
|
when RBS::AST::Declarations::Class
|
@@ -91,7 +105,7 @@ module Steep
|
|
91
105
|
container_name = entry.method_name.type_name.relative!.to_s
|
92
106
|
|
93
107
|
entry.declarations.each do |decl|
|
94
|
-
next unless
|
108
|
+
next unless assigned?(Pathname(decl.location.buffer.name))
|
95
109
|
|
96
110
|
case decl
|
97
111
|
when RBS::AST::Members::MethodDefinition
|
@@ -130,7 +144,7 @@ module Steep
|
|
130
144
|
next unless SignatureSymbolProvider.test_const_name(query, entry.const_name)
|
131
145
|
|
132
146
|
entry.declarations.each do |decl|
|
133
|
-
next unless
|
147
|
+
next unless assigned?(Pathname(decl.location.buffer.name))
|
134
148
|
|
135
149
|
symbols << SymbolInformation.new(
|
136
150
|
name: entry.const_name.name.to_s,
|
@@ -143,7 +157,7 @@ module Steep
|
|
143
157
|
next unless SignatureSymbolProvider.test_global_name(query, entry.global_name)
|
144
158
|
|
145
159
|
entry.declarations.each do |decl|
|
146
|
-
next unless
|
160
|
+
next unless assigned?(Pathname(decl.location.buffer.name))
|
147
161
|
|
148
162
|
symbols << SymbolInformation.new(
|
149
163
|
name: decl.name.to_s,
|
data/lib/steep/project/target.rb
CHANGED
@@ -25,14 +25,14 @@ module Steep
|
|
25
25
|
signature_pattern =~ path
|
26
26
|
end
|
27
27
|
|
28
|
-
def new_env_loader
|
29
|
-
Target.construct_env_loader(options: options)
|
28
|
+
def new_env_loader(project:)
|
29
|
+
Target.construct_env_loader(options: options, project: project)
|
30
30
|
end
|
31
31
|
|
32
|
-
def self.construct_env_loader(options:)
|
32
|
+
def self.construct_env_loader(options:, project:)
|
33
33
|
repo = RBS::Repository.new(no_stdlib: options.vendor_path)
|
34
34
|
options.repository_paths.each do |path|
|
35
|
-
repo.add(path)
|
35
|
+
repo.add(project.absolute_path(path))
|
36
36
|
end
|
37
37
|
|
38
38
|
loader = RBS::EnvironmentLoader.new(
|
@@ -12,8 +12,7 @@ module Steep
|
|
12
12
|
def initialize(project:, reader:, writer:, queue: Queue.new)
|
13
13
|
super(project: project, reader: reader, writer: writer)
|
14
14
|
@queue = queue
|
15
|
-
@service = Services::TypeCheckService.new(project: project
|
16
|
-
service.no_type_checking!
|
15
|
+
@service = Services::TypeCheckService.new(project: project)
|
17
16
|
@mutex = Mutex.new
|
18
17
|
@buffered_changes = {}
|
19
18
|
end
|
@@ -24,7 +23,7 @@ module Steep
|
|
24
23
|
|
25
24
|
unless changes.empty?
|
26
25
|
Steep.logger.debug { "Applying changes for #{changes.size} files..." }
|
27
|
-
service.update(changes: changes)
|
26
|
+
service.update(changes: changes)
|
28
27
|
end
|
29
28
|
|
30
29
|
case job
|
data/lib/steep/server/master.rb
CHANGED
@@ -3,109 +3,372 @@ module Steep
|
|
3
3
|
class Master
|
4
4
|
LSP = LanguageServer::Protocol
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
class TypeCheckRequest
|
7
|
+
attr_reader :guid
|
8
|
+
attr_reader :library_paths
|
9
|
+
attr_reader :signature_paths
|
10
|
+
attr_reader :code_paths
|
11
|
+
attr_reader :priority_paths
|
12
|
+
attr_reader :checked_paths
|
13
|
+
|
14
|
+
def initialize(guid:)
|
15
|
+
@guid = guid
|
16
|
+
@library_paths = Set[]
|
17
|
+
@signature_paths = Set[]
|
18
|
+
@code_paths = Set[]
|
19
|
+
@priority_paths = Set[]
|
20
|
+
@checked_paths = Set[]
|
21
|
+
end
|
9
22
|
|
10
|
-
|
11
|
-
|
23
|
+
def uri(path)
|
24
|
+
URI.parse(path.to_s).tap do |uri|
|
25
|
+
uri.scheme = "file"
|
26
|
+
end
|
27
|
+
end
|
12
28
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
# We have two queues:
|
23
|
-
#
|
24
|
-
# 1. `recon_queue` is to pass messages from worker threads to reconciliation thread
|
25
|
-
# 2. `write` thread is to pass messages to write thread
|
26
|
-
#
|
27
|
-
# Message passing: Client -> Server (Master) -> Worker
|
28
|
-
#
|
29
|
-
# 1. Client -> Server
|
30
|
-
# Master receives messages from the LSP client on main thread.
|
31
|
-
#
|
32
|
-
# 2. Master -> Worker
|
33
|
-
# Master writes messages to workers on main thread.
|
34
|
-
#
|
35
|
-
# Message passing: Worker -> Server (Master) -> (reconciliation queue) -> (write queue) -> Client
|
36
|
-
#
|
37
|
-
# 3. Worker -> Master
|
38
|
-
# Master receives messages on threads dedicated for each worker.
|
39
|
-
# The messages sent from workers are then forwarded to the reconciliation thread through reconciliation queue.
|
40
|
-
#
|
41
|
-
# 4. Server -> Client
|
42
|
-
# The reconciliation thread reads messages from reconciliation queue, does something, and finally sends messages to the client via write queue.
|
43
|
-
#
|
44
|
-
attr_reader :write_queue
|
45
|
-
attr_reader :recon_queue
|
46
|
-
|
47
|
-
class ResponseHandler
|
48
|
-
attr_reader :workers
|
29
|
+
def as_json(assignment:)
|
30
|
+
{
|
31
|
+
guid: guid,
|
32
|
+
library_uris: library_paths.grep(assignment).map {|path| uri(path).to_s },
|
33
|
+
signature_uris: signature_paths.grep(assignment).map {|path| uri(path).to_s },
|
34
|
+
code_uris: code_paths.grep(assignment).map {|path| uri(path).to_s },
|
35
|
+
priority_uris: priority_paths.map {|path| uri(path).to_s }
|
36
|
+
}
|
37
|
+
end
|
49
38
|
|
50
|
-
|
51
|
-
|
39
|
+
def total
|
40
|
+
library_paths.size + signature_paths.size + code_paths.size
|
41
|
+
end
|
52
42
|
|
53
|
-
|
54
|
-
|
43
|
+
def percentage
|
44
|
+
checked_paths.size * 100 / total
|
45
|
+
end
|
55
46
|
|
56
|
-
def
|
57
|
-
|
47
|
+
def all_paths
|
48
|
+
library_paths + signature_paths + code_paths
|
49
|
+
end
|
58
50
|
|
59
|
-
|
60
|
-
|
61
|
-
|
51
|
+
def checking_path?(path)
|
52
|
+
library_paths.include?(path) ||
|
53
|
+
signature_paths.include?(path) ||
|
54
|
+
code_paths.include?(path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def checked(path)
|
58
|
+
raise unless checking_path?(path)
|
59
|
+
checked_paths << path
|
60
|
+
end
|
61
|
+
|
62
|
+
def finished?
|
63
|
+
unchecked_paths.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
def unchecked_paths
|
67
|
+
all_paths - checked_paths
|
68
|
+
end
|
69
|
+
|
70
|
+
def unchecked_code_paths
|
71
|
+
code_paths - checked_paths
|
72
|
+
end
|
73
|
+
|
74
|
+
def unchecked_library_paths
|
75
|
+
library_paths - checked_paths
|
76
|
+
end
|
77
|
+
|
78
|
+
def unchecked_signature_paths
|
79
|
+
signature_paths - checked_paths
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class TypeCheckController
|
84
|
+
attr_reader :project
|
85
|
+
attr_reader :priority_paths
|
86
|
+
attr_reader :changed_paths
|
87
|
+
attr_reader :target_paths
|
88
|
+
|
89
|
+
class TargetPaths
|
90
|
+
attr_reader :project
|
91
|
+
attr_reader :target
|
92
|
+
attr_reader :code_paths
|
93
|
+
attr_reader :signature_paths
|
94
|
+
attr_reader :library_paths
|
95
|
+
|
96
|
+
def initialize(project:, target:)
|
97
|
+
@project = project
|
98
|
+
@target = target
|
99
|
+
@code_paths = Set[]
|
100
|
+
@signature_paths = Set[]
|
101
|
+
@library_paths = Set[]
|
62
102
|
end
|
63
103
|
|
64
|
-
|
65
|
-
|
104
|
+
def all_paths
|
105
|
+
code_paths + signature_paths + library_paths
|
106
|
+
end
|
107
|
+
|
108
|
+
def library_path?(path)
|
109
|
+
library_paths.include?(path)
|
110
|
+
end
|
111
|
+
|
112
|
+
def signature_path?(path)
|
113
|
+
signature_paths.include?(path)
|
114
|
+
end
|
115
|
+
|
116
|
+
def code_path?(path)
|
117
|
+
code_paths.include?(path)
|
118
|
+
end
|
119
|
+
|
120
|
+
def add(path)
|
121
|
+
return if library_path?(path) || signature_path?(path) || code_path?(path)
|
122
|
+
|
123
|
+
relative_path = project.relative_path(path)
|
124
|
+
|
125
|
+
case
|
126
|
+
when target.source_pattern =~ relative_path
|
127
|
+
code_paths << path
|
128
|
+
when target.signature_pattern =~ relative_path
|
129
|
+
signature_paths << path
|
130
|
+
else
|
131
|
+
library_paths << path
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
alias << add
|
66
136
|
end
|
67
137
|
|
68
|
-
def
|
69
|
-
|
138
|
+
def initialize(project:)
|
139
|
+
@project = project
|
140
|
+
@priority_paths = Set[]
|
141
|
+
@changed_paths = Set[]
|
142
|
+
@target_paths = project.targets.each.map {|target| TargetPaths.new(project: project, target: target) }
|
70
143
|
end
|
71
144
|
|
72
|
-
def
|
73
|
-
|
145
|
+
def load(command_line_args:)
|
146
|
+
loader = Services::FileLoader.new(base_dir: project.base_dir)
|
147
|
+
|
148
|
+
target_paths.each do |paths|
|
149
|
+
target = paths.target
|
150
|
+
|
151
|
+
signature_service = Services::SignatureService.load_from(target.new_env_loader(project: project))
|
152
|
+
paths.library_paths.merge(signature_service.env_rbs_paths)
|
153
|
+
|
154
|
+
loader.each_path_in_patterns(target.source_pattern, command_line_args) do |path|
|
155
|
+
paths.code_paths << project.absolute_path(path)
|
156
|
+
end
|
157
|
+
loader.each_path_in_patterns(target.signature_pattern) do |path|
|
158
|
+
paths.signature_paths << project.absolute_path(path)
|
159
|
+
end
|
160
|
+
|
161
|
+
changed_paths.merge(paths.all_paths)
|
162
|
+
end
|
74
163
|
end
|
75
164
|
|
76
|
-
def
|
77
|
-
|
165
|
+
def push_changes(path)
|
166
|
+
return if target_paths.any? {|paths| paths.library_path?(path) }
|
167
|
+
|
168
|
+
target_paths.each {|paths| paths << path }
|
169
|
+
|
170
|
+
if target_paths.any? {|paths| paths.code_path?(path) || paths.signature_path?(path) }
|
171
|
+
changed_paths << path
|
172
|
+
end
|
78
173
|
end
|
79
174
|
|
80
|
-
def
|
81
|
-
|
175
|
+
def update_priority(open: nil, close: nil)
|
176
|
+
path = open || close
|
177
|
+
|
178
|
+
target_paths.each {|paths| paths << path }
|
82
179
|
|
83
|
-
|
84
|
-
|
180
|
+
case
|
181
|
+
when open
|
182
|
+
priority_paths << path
|
183
|
+
when close
|
184
|
+
priority_paths.delete path
|
85
185
|
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def make_request(guid: SecureRandom.uuid, last_request: nil, include_unchanged: false)
|
189
|
+
return if changed_paths.empty? && !include_unchanged
|
190
|
+
|
191
|
+
TypeCheckRequest.new(guid: guid).tap do |request|
|
192
|
+
if last_request
|
193
|
+
request.library_paths.merge(last_request.unchecked_library_paths)
|
194
|
+
request.signature_paths.merge(last_request.unchecked_signature_paths)
|
195
|
+
request.code_paths.merge(last_request.unchecked_code_paths)
|
196
|
+
end
|
197
|
+
|
198
|
+
if include_unchanged
|
199
|
+
target_paths.each do |paths|
|
200
|
+
request.signature_paths.merge(paths.signature_paths)
|
201
|
+
request.library_paths.merge(paths.library_paths)
|
202
|
+
request.code_paths.merge(paths.code_paths)
|
203
|
+
end
|
204
|
+
else
|
205
|
+
updated_paths = target_paths.select {|paths| changed_paths.intersect?(paths.all_paths) }
|
206
|
+
|
207
|
+
updated_paths.each do |paths|
|
208
|
+
case
|
209
|
+
when paths.signature_paths.intersect?(changed_paths)
|
210
|
+
request.signature_paths.merge(paths.signature_paths)
|
211
|
+
request.library_paths.merge(paths.library_paths)
|
212
|
+
request.code_paths.merge(paths.code_paths)
|
213
|
+
when paths.code_paths.intersect?(changed_paths)
|
214
|
+
request.code_paths.merge(paths.code_paths & changed_paths)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
request.priority_paths.merge(priority_paths)
|
220
|
+
|
221
|
+
changed_paths.clear()
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class ResultHandler
|
227
|
+
attr_reader :request
|
228
|
+
attr_reader :completion_handler
|
229
|
+
attr_reader :response
|
230
|
+
|
231
|
+
def initialize(request:)
|
232
|
+
@request = request
|
233
|
+
@response = nil
|
234
|
+
@completion_handler = nil
|
235
|
+
@completed = false
|
236
|
+
end
|
237
|
+
|
238
|
+
def process_response(message)
|
239
|
+
if request[:id] == message[:id]
|
240
|
+
completion_handler&.call(message)
|
241
|
+
@response = message
|
242
|
+
true
|
243
|
+
else
|
244
|
+
false
|
245
|
+
end
|
246
|
+
end
|
86
247
|
|
87
|
-
|
88
|
-
|
89
|
-
|
248
|
+
def result
|
249
|
+
response&.dig(:result)
|
250
|
+
end
|
251
|
+
|
252
|
+
def completed?
|
253
|
+
!!@response
|
254
|
+
end
|
255
|
+
|
256
|
+
def on_completion(&block)
|
257
|
+
@completion_handler = block
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class GroupHandler
|
262
|
+
attr_reader :request
|
263
|
+
attr_reader :handlers
|
264
|
+
attr_reader :completion_handler
|
265
|
+
|
266
|
+
def initialize()
|
267
|
+
@handlers = {}
|
268
|
+
@waiting_handlers = Set[]
|
269
|
+
@completion_handler = nil
|
270
|
+
end
|
271
|
+
|
272
|
+
def process_response(message)
|
273
|
+
if handler = handlers[message[:id]]
|
274
|
+
handler.process_response(message)
|
275
|
+
|
276
|
+
if completed?
|
277
|
+
completion_handler&.call(handlers.values)
|
90
278
|
end
|
279
|
+
|
280
|
+
true
|
281
|
+
else
|
282
|
+
false
|
91
283
|
end
|
92
284
|
end
|
93
285
|
|
94
286
|
def completed?
|
95
|
-
|
287
|
+
handlers.each_value.all? {|handler| handler.completed? }
|
288
|
+
end
|
289
|
+
|
290
|
+
def <<(handler)
|
291
|
+
handlers[handler.request[:id]] = handler
|
292
|
+
end
|
293
|
+
|
294
|
+
def on_completion(&block)
|
295
|
+
@completion_handler = block
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
class ResultController
|
300
|
+
attr_reader :handlers
|
301
|
+
|
302
|
+
def initialize()
|
303
|
+
@handlers = []
|
304
|
+
end
|
305
|
+
|
306
|
+
def <<(handler)
|
307
|
+
@handlers << handler
|
308
|
+
end
|
309
|
+
|
310
|
+
def request_group()
|
311
|
+
group = GroupHandler.new()
|
312
|
+
yield group
|
313
|
+
group
|
314
|
+
end
|
315
|
+
|
316
|
+
def process_response(message)
|
317
|
+
handlers.each do |handler|
|
318
|
+
return true if handler.process_response(message)
|
319
|
+
end
|
320
|
+
false
|
321
|
+
ensure
|
322
|
+
handlers.reject!(&:completed?)
|
96
323
|
end
|
97
324
|
end
|
98
325
|
|
326
|
+
ReceiveMessageJob = Struct.new(:source, :message, keyword_init: true) do
|
327
|
+
def response?
|
328
|
+
message.key?(:id) && !message.key?(:method)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
SendMessageJob = Struct.new(:dest, :message, keyword_init: true) do
|
333
|
+
def self.to_worker(worker, message:)
|
334
|
+
new(dest: worker, message: message)
|
335
|
+
end
|
336
|
+
|
337
|
+
def self.to_client(message:)
|
338
|
+
new(dest: :client, message: message)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
attr_reader :steepfile
|
343
|
+
attr_reader :project
|
344
|
+
attr_reader :reader, :writer
|
345
|
+
attr_reader :commandline_args
|
346
|
+
|
347
|
+
attr_reader :interaction_worker
|
348
|
+
attr_reader :typecheck_workers
|
349
|
+
|
350
|
+
attr_reader :job_queue
|
351
|
+
|
352
|
+
attr_reader :current_type_check_request
|
353
|
+
attr_reader :controller
|
354
|
+
attr_reader :result_controller
|
355
|
+
|
356
|
+
attr_reader :initialize_params
|
357
|
+
attr_accessor :typecheck_automatically
|
358
|
+
|
99
359
|
def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new)
|
100
360
|
@project = project
|
101
361
|
@reader = reader
|
102
362
|
@writer = writer
|
103
|
-
@write_queue = queue
|
104
|
-
@recon_queue = Queue.new
|
105
363
|
@interaction_worker = interaction_worker
|
106
364
|
@typecheck_workers = typecheck_workers
|
107
|
-
@
|
108
|
-
@
|
365
|
+
@current_type_check_request = nil
|
366
|
+
@typecheck_automatically = true
|
367
|
+
@commandline_args = []
|
368
|
+
@job_queue = queue
|
369
|
+
|
370
|
+
@controller = TypeCheckController.new(project: project)
|
371
|
+
@result_controller = ResultController.new()
|
109
372
|
end
|
110
373
|
|
111
374
|
def start
|
@@ -118,7 +381,7 @@ module Steep
|
|
118
381
|
worker_threads << Thread.new do
|
119
382
|
Steep.logger.formatter.push_tags(*tags, "from-worker@interaction")
|
120
383
|
interaction_worker.reader.read do |message|
|
121
|
-
|
384
|
+
job_queue << ReceiveMessageJob.new(source: interaction_worker, message: message)
|
122
385
|
end
|
123
386
|
end
|
124
387
|
end
|
@@ -127,42 +390,57 @@ module Steep
|
|
127
390
|
worker_threads << Thread.new do
|
128
391
|
Steep.logger.formatter.push_tags(*tags, "from-worker@#{worker.name}")
|
129
392
|
worker.reader.read do |message|
|
130
|
-
|
393
|
+
job_queue << ReceiveMessageJob.new(source: worker, message: message)
|
131
394
|
end
|
132
395
|
end
|
133
396
|
end
|
134
397
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
end
|
140
|
-
|
141
|
-
writer.io.close
|
142
|
-
end
|
143
|
-
|
144
|
-
worker_threads << Thread.new do
|
145
|
-
Steep.logger.formatter.push_tags(*tags, "reconciliation")
|
146
|
-
while (message, worker = recon_queue.pop)
|
147
|
-
id = message[:id]
|
148
|
-
handler = response_handlers[id] or raise
|
149
|
-
|
150
|
-
Steep.logger.info "Processing response to #{handler.request[:method]}(#{id}) from #{worker.name}"
|
151
|
-
|
152
|
-
handler.process_response(message, worker)
|
153
|
-
|
154
|
-
if handler.completed?
|
155
|
-
Steep.logger.info "Response to #{handler.request[:method]}(#{id}) completed"
|
156
|
-
response_handlers.delete(id)
|
157
|
-
end
|
398
|
+
read_client_thread = Thread.new do
|
399
|
+
reader.read do |message|
|
400
|
+
job_queue << ReceiveMessageJob.new(source: :client, message: message)
|
401
|
+
break if message[:method] == "exit"
|
158
402
|
end
|
159
403
|
end
|
160
404
|
|
161
405
|
Steep.logger.tagged "main" do
|
162
|
-
|
163
|
-
|
406
|
+
while job = job_queue.deq
|
407
|
+
case job
|
408
|
+
when ReceiveMessageJob
|
409
|
+
src = if job.source == :client
|
410
|
+
:client
|
411
|
+
else
|
412
|
+
job.source.name
|
413
|
+
end
|
414
|
+
Steep.logger.tagged("ReceiveMessageJob(#{src}/#{job.message[:method]}/#{job.message[:id]})") do
|
415
|
+
if job.response? && result_controller.process_response(job.message)
|
416
|
+
# nop
|
417
|
+
Steep.logger.info { "Processed by ResultController" }
|
418
|
+
else
|
419
|
+
case job.source
|
420
|
+
when :client
|
421
|
+
process_message_from_client(job.message)
|
422
|
+
|
423
|
+
if job.message[:method] == "exit"
|
424
|
+
job_queue.close()
|
425
|
+
end
|
426
|
+
when WorkerProcess
|
427
|
+
process_message_from_worker(job.message, worker: job.source)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
when SendMessageJob
|
432
|
+
case job.dest
|
433
|
+
when :client
|
434
|
+
Steep.logger.info { "Processing SendMessageJob: dest=client, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
435
|
+
writer.write job.message
|
436
|
+
when WorkerProcess
|
437
|
+
Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
438
|
+
job.dest << job.message
|
439
|
+
end
|
440
|
+
end
|
164
441
|
end
|
165
442
|
|
443
|
+
read_client_thread.join()
|
166
444
|
worker_threads.each do |thread|
|
167
445
|
thread.join
|
168
446
|
end
|
@@ -179,138 +457,311 @@ module Steep
|
|
179
457
|
end
|
180
458
|
end
|
181
459
|
|
460
|
+
def pathname(uri)
|
461
|
+
Pathname(URI.parse(uri).path)
|
462
|
+
end
|
463
|
+
|
464
|
+
def work_done_progress_supported?
|
465
|
+
initialize_params&.dig(:capabilities, :window, :workDoneProgress)
|
466
|
+
end
|
467
|
+
|
182
468
|
def process_message_from_client(message)
|
183
|
-
Steep.logger.info "
|
469
|
+
Steep.logger.info "Processing message from client: method=#{message[:method]}, id=#{message[:id]}"
|
184
470
|
id = message[:id]
|
185
471
|
|
186
472
|
case message[:method]
|
187
473
|
when "initialize"
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
474
|
+
@initialize_params = message[:params]
|
475
|
+
result_controller << group_request do |group|
|
476
|
+
each_worker do |worker|
|
477
|
+
group << send_request(method: "initialize", params: message[:params], worker: worker)
|
478
|
+
end
|
479
|
+
|
480
|
+
group.on_completion do
|
481
|
+
controller.load(command_line_args: commandline_args)
|
482
|
+
|
483
|
+
job_queue << SendMessageJob.to_client(
|
484
|
+
message: {
|
485
|
+
id: id,
|
486
|
+
result: LSP::Interface::InitializeResult.new(
|
487
|
+
capabilities: LSP::Interface::ServerCapabilities.new(
|
488
|
+
text_document_sync: LSP::Interface::TextDocumentSyncOptions.new(
|
489
|
+
change: LSP::Constant::TextDocumentSyncKind::INCREMENTAL,
|
490
|
+
save: LSP::Interface::SaveOptions.new(include_text: false),
|
491
|
+
open_close: true
|
492
|
+
),
|
493
|
+
hover_provider: {
|
494
|
+
workDoneProgress: true,
|
495
|
+
partialResults: true,
|
496
|
+
partialResult: true
|
497
|
+
},
|
498
|
+
completion_provider: LSP::Interface::CompletionOptions.new(
|
499
|
+
trigger_characters: [".", "@"],
|
500
|
+
work_done_progress: true
|
501
|
+
),
|
502
|
+
workspace_symbol_provider: true
|
503
|
+
)
|
202
504
|
)
|
203
|
-
|
204
|
-
|
505
|
+
}
|
506
|
+
)
|
205
507
|
end
|
206
508
|
end
|
207
509
|
|
510
|
+
when "initialized"
|
511
|
+
if typecheck_automatically
|
512
|
+
request = controller.make_request(include_unchanged: true)
|
513
|
+
start_type_check(request, last_request: nil, start_progress: request.total > 10)
|
514
|
+
end
|
515
|
+
|
208
516
|
when "textDocument/didChange"
|
209
517
|
broadcast_notification(message)
|
518
|
+
path = pathname(message[:params][:textDocument][:uri])
|
519
|
+
controller.push_changes(path)
|
520
|
+
|
521
|
+
when "textDocument/didSave"
|
522
|
+
if typecheck_automatically
|
523
|
+
request = controller.make_request(last_request: current_type_check_request)
|
524
|
+
start_type_check(
|
525
|
+
request,
|
526
|
+
last_request: current_type_check_request,
|
527
|
+
start_progress: request.total > 10
|
528
|
+
)
|
529
|
+
end
|
530
|
+
|
531
|
+
when "textDocument/didOpen"
|
532
|
+
path = pathname(message[:params][:textDocument][:uri])
|
533
|
+
controller.update_priority(open: path)
|
534
|
+
|
535
|
+
when "textDocument/didClose"
|
536
|
+
path = pathname(message[:params][:textDocument][:uri])
|
537
|
+
controller.update_priority(close: path)
|
210
538
|
|
211
539
|
when "textDocument/hover", "textDocument/completion"
|
212
540
|
if interaction_worker
|
213
|
-
send_request(message, worker: interaction_worker) do |handler|
|
541
|
+
result_controller << send_request(method: message[:method], params: message[:params], worker: interaction_worker) do |handler|
|
214
542
|
handler.on_completion do |response|
|
215
|
-
|
543
|
+
job_queue << SendMessageJob.to_client(
|
544
|
+
message: {
|
545
|
+
id: message[:id],
|
546
|
+
result: response[:result]
|
547
|
+
}
|
548
|
+
)
|
216
549
|
end
|
217
550
|
end
|
218
551
|
end
|
219
552
|
|
220
|
-
when "textDocument/open"
|
221
|
-
# Ignores open notification
|
222
|
-
|
223
553
|
when "workspace/symbol"
|
224
|
-
|
225
|
-
|
226
|
-
|
554
|
+
result_controller << group_request do |group|
|
555
|
+
typecheck_workers.each do |worker|
|
556
|
+
group << send_request(method: "workspace/symbol", params: message[:params], worker: worker)
|
557
|
+
end
|
227
558
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
}
|
559
|
+
group.on_completion do |handlers|
|
560
|
+
result = handlers.flat_map(&:result)
|
561
|
+
job_queue << SendMessageJob.to_client(message: { id: message[:id], result: result })
|
232
562
|
end
|
233
563
|
end
|
234
564
|
|
235
565
|
when "workspace/executeCommand"
|
236
566
|
case message[:params][:command]
|
237
567
|
when "steep/stats"
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
568
|
+
result_controller << group_request do |group|
|
569
|
+
typecheck_workers.each do |worker|
|
570
|
+
group << send_request(method: "workspace/executeCommand", params: message[:params], worker: worker)
|
571
|
+
end
|
572
|
+
|
573
|
+
group.on_completion do |handlers|
|
574
|
+
stats = handlers.flat_map(&:result)
|
575
|
+
job_queue << SendMessageJob.to_client(
|
576
|
+
message: {
|
577
|
+
id: message[:id],
|
578
|
+
result: stats
|
579
|
+
}
|
580
|
+
)
|
245
581
|
end
|
246
582
|
end
|
247
583
|
end
|
248
584
|
|
585
|
+
when "$/typecheck"
|
586
|
+
request = controller.make_request(
|
587
|
+
guid: message[:params][:guid],
|
588
|
+
last_request: current_type_check_request,
|
589
|
+
include_unchanged: true
|
590
|
+
)
|
591
|
+
start_type_check(
|
592
|
+
request,
|
593
|
+
last_request: current_type_check_request,
|
594
|
+
start_progress: true
|
595
|
+
)
|
596
|
+
|
249
597
|
when "shutdown"
|
250
|
-
|
251
|
-
|
252
|
-
|
598
|
+
result_controller << group_request do |group|
|
599
|
+
each_worker do |worker|
|
600
|
+
group << send_request(method: "shutdown", worker: worker)
|
601
|
+
end
|
253
602
|
|
254
|
-
|
255
|
-
|
603
|
+
group.on_completion do
|
604
|
+
job_queue << SendMessageJob.to_client(message: { id: message[:id], result: nil })
|
256
605
|
end
|
257
606
|
end
|
258
607
|
|
259
608
|
when "exit"
|
260
609
|
broadcast_notification(message)
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
def process_message_from_worker(message, worker:)
|
614
|
+
Steep.logger.tagged "#process_message_from_worker (worker=#{worker.name})" do
|
615
|
+
Steep.logger.info { "Processing message from worker: method=#{message[:method] || "-"}, id=#{message[:id] || "*"}" }
|
616
|
+
|
617
|
+
case
|
618
|
+
when message.key?(:id) && !message.key?(:method)
|
619
|
+
Steep.logger.tagged "response(id=#{message[:id]})" do
|
620
|
+
Steep.logger.error { "Received unexpected response" }
|
621
|
+
Steep.logger.debug { "result = #{message[:result].inspect}" }
|
622
|
+
end
|
623
|
+
when message.key?(:method) && !message.key?(:id)
|
624
|
+
case message[:method]
|
625
|
+
when "$/typecheck/progress"
|
626
|
+
on_type_check_update(
|
627
|
+
guid: message[:params][:guid],
|
628
|
+
path: Pathname(message[:params][:path])
|
629
|
+
)
|
630
|
+
else
|
631
|
+
# Forward other notifications
|
632
|
+
job_queue << SendMessageJob.to_client(message: message)
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
def start_type_check(request, last_request:, start_progress:)
|
639
|
+
Steep.logger.tagged "#start_type_check(#{request.guid}, #{last_request&.guid}" do
|
640
|
+
unless request
|
641
|
+
Steep.logger.info "Skip start type checking"
|
642
|
+
return
|
643
|
+
end
|
644
|
+
|
645
|
+
if last_request
|
646
|
+
Steep.logger.info "Cancelling last request"
|
647
|
+
|
648
|
+
job_queue << SendMessageJob.to_client(
|
649
|
+
message: {
|
650
|
+
method: "$/progress",
|
651
|
+
params: {
|
652
|
+
token: last_request.guid,
|
653
|
+
value: { kind: "end" }
|
654
|
+
}
|
655
|
+
}
|
656
|
+
)
|
657
|
+
end
|
658
|
+
|
659
|
+
if start_progress
|
660
|
+
Steep.logger.info "Starting new progress..."
|
261
661
|
|
262
|
-
|
662
|
+
@current_type_check_request = request
|
663
|
+
|
664
|
+
if work_done_progress_supported?
|
665
|
+
job_queue << SendMessageJob.to_client(
|
666
|
+
message: {
|
667
|
+
id: fresh_request_id,
|
668
|
+
method: "window/workDoneProgress/create",
|
669
|
+
params: { token: request.guid }
|
670
|
+
}
|
671
|
+
)
|
672
|
+
end
|
673
|
+
|
674
|
+
job_queue << SendMessageJob.to_client(
|
675
|
+
message: {
|
676
|
+
method: "$/progress",
|
677
|
+
params: {
|
678
|
+
token: request.guid,
|
679
|
+
value: { kind: "begin", title: "Type checking", percentage: 0 }
|
680
|
+
}
|
681
|
+
}
|
682
|
+
)
|
683
|
+
|
684
|
+
if request.finished?
|
685
|
+
job_queue << SendMessageJob.to_client(
|
686
|
+
message: {
|
687
|
+
method: "$/progress",
|
688
|
+
params: { token: request.guid, value: { kind: "end" } }
|
689
|
+
}
|
690
|
+
)
|
691
|
+
end
|
692
|
+
else
|
693
|
+
@current_type_check_request = nil
|
694
|
+
end
|
695
|
+
|
696
|
+
Steep.logger.info "Sending $/typecheck/start notifications"
|
697
|
+
typecheck_workers.each do |worker|
|
698
|
+
assignment = Services::PathAssignment.new(max_index: typecheck_workers.size, index: worker.index)
|
699
|
+
|
700
|
+
job_queue << SendMessageJob.to_worker(
|
701
|
+
worker,
|
702
|
+
message: {
|
703
|
+
method: "$/typecheck/start",
|
704
|
+
params: request.as_json(assignment: assignment)
|
705
|
+
}
|
706
|
+
)
|
707
|
+
end
|
263
708
|
end
|
709
|
+
end
|
264
710
|
|
265
|
-
|
711
|
+
def on_type_check_update(guid:, path:)
|
712
|
+
if current = current_type_check_request()
|
713
|
+
if current.guid == guid
|
714
|
+
current.checked(path)
|
715
|
+
Steep.logger.info { "Request updated: checked=#{path}, unchecked=#{current.unchecked_paths.size}" }
|
716
|
+
percentage = current.percentage
|
717
|
+
value = if percentage == 100
|
718
|
+
{ kind: "end" }
|
719
|
+
else
|
720
|
+
progress_string = ("▮"*(percentage/5)) + ("▯"*(20 - percentage/5))
|
721
|
+
{ kind: "report", percentage: percentage, message: "#{progress_string} (#{percentage}%)" }
|
722
|
+
end
|
723
|
+
|
724
|
+
job_queue << SendMessageJob.to_client(
|
725
|
+
message: {
|
726
|
+
method: "$/progress",
|
727
|
+
params: { token: current.guid, value: value }
|
728
|
+
}
|
729
|
+
)
|
730
|
+
|
731
|
+
@current_type_check_request = nil if current.finished?
|
732
|
+
end
|
733
|
+
end
|
266
734
|
end
|
267
735
|
|
268
736
|
def broadcast_notification(message)
|
269
737
|
Steep.logger.info "Broadcasting notification #{message[:method]}"
|
270
738
|
each_worker do |worker|
|
271
|
-
|
739
|
+
job_queue << SendMessageJob.new(dest: worker, message: message)
|
272
740
|
end
|
273
741
|
end
|
274
742
|
|
275
743
|
def send_notification(message, worker:)
|
276
744
|
Steep.logger.info "Sending notification #{message[:method]} to #{worker.name}"
|
277
|
-
|
745
|
+
job_queue << SendMessageJob.new(dest: worker, message: message)
|
278
746
|
end
|
279
747
|
|
280
|
-
def
|
281
|
-
|
282
|
-
|
283
|
-
Steep.logger.info "Sending request #{message[:method]}(#{message[:id]}) to #{workers.map(&:name).join(", ")}"
|
284
|
-
handler = ResponseHandler.new(request: message, workers: workers)
|
285
|
-
yield(handler) if block_given?
|
286
|
-
response_handlers[handler.request_id] = handler
|
287
|
-
|
288
|
-
workers.each do |w|
|
289
|
-
w << message
|
290
|
-
end
|
748
|
+
def fresh_request_id
|
749
|
+
SecureRandom.alphanumeric(10)
|
291
750
|
end
|
292
751
|
|
293
|
-
def
|
294
|
-
Steep.logger.info "
|
295
|
-
handler = ResponseHandler.new(request: message, workers: each_worker.to_a)
|
296
|
-
yield(handler) if block_given?
|
297
|
-
response_handlers[handler.request_id] = handler
|
752
|
+
def send_request(method:, id: fresh_request_id(), params: nil, worker:, &block)
|
753
|
+
Steep.logger.info "Sending request #{method}(#{id}) to #{worker.name}"
|
298
754
|
|
299
|
-
|
300
|
-
|
755
|
+
message = { method: method, id: id, params: params }
|
756
|
+
ResultHandler.new(request: message).tap do |handler|
|
757
|
+
yield handler if block_given?
|
758
|
+
job_queue << SendMessageJob.to_worker(worker, message: message)
|
301
759
|
end
|
302
760
|
end
|
303
761
|
|
304
|
-
def
|
305
|
-
|
306
|
-
|
307
|
-
# Response from worker
|
308
|
-
Steep.logger.debug { "Received response #{message[:id]} from worker" }
|
309
|
-
recon_queue << [message, worker]
|
310
|
-
when message.key?(:method) && !message.key?(:id)
|
311
|
-
# Notification from worker
|
312
|
-
Steep.logger.debug { "Received notification #{message[:method]} from worker" }
|
313
|
-
write_queue << message
|
762
|
+
def group_request()
|
763
|
+
GroupHandler.new().tap do |group|
|
764
|
+
yield group
|
314
765
|
end
|
315
766
|
end
|
316
767
|
|