steep 2.0.0 → 2.1.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 +35 -0
- data/CLAUDE.md +2 -2
- data/README.md +3 -3
- data/Rakefile +350 -147
- data/bin/steep-check.rb +4 -4
- data/doc/release.md +264 -0
- data/lib/steep/ast/types/helper.rb +0 -1
- data/lib/steep/ast/types/name.rb +1 -1
- data/lib/steep/cli.rb +32 -3
- data/lib/steep/daemon.rb +49 -9
- data/lib/steep/diagnostic/ruby.rb +4 -4
- data/lib/steep/drivers/init.rb +4 -3
- data/lib/steep/drivers/langserver.rb +33 -1
- data/lib/steep/drivers/query.rb +40 -2
- data/lib/steep/interface/builder.rb +24 -4
- data/lib/steep/node_helper.rb +2 -2
- data/lib/steep/server/base_worker.rb +15 -0
- data/lib/steep/server/command_socket.rb +181 -0
- data/lib/steep/server/custom_methods.rb +12 -0
- data/lib/steep/server/master.rb +383 -2
- data/lib/steep/server/type_check_worker.rb +48 -2
- data/lib/steep/services/goto_service.rb +7 -3
- data/lib/steep/services/hover_provider/content.rb +1 -1
- data/lib/steep/services/hover_provider/ruby.rb +1 -1
- data/lib/steep/services/signature_help_provider.rb +1 -1
- data/lib/steep/source.rb +9 -10
- data/lib/steep/subtyping/check.rb +6 -3
- data/lib/steep/tagged_logging.rb +4 -1
- data/lib/steep/type_construction.rb +37 -16
- data/lib/steep/type_inference/case_when.rb +2 -2
- data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
- data/lib/steep/type_inference/send_args.rb +9 -5
- data/lib/steep/typing.rb +3 -3
- data/lib/steep/version.rb +1 -1
- data/lib/steep.rb +1 -0
- data/steep.gemspec +2 -8
- metadata +7 -5
data/lib/steep/server/master.rb
CHANGED
|
@@ -186,6 +186,12 @@ module Steep
|
|
|
186
186
|
attr_accessor :typecheck_automatically
|
|
187
187
|
attr_reader :start_type_checking_queue
|
|
188
188
|
|
|
189
|
+
# Type check requests waiting for the current type check to finish
|
|
190
|
+
attr_reader :pending_typecheck_requests
|
|
191
|
+
|
|
192
|
+
# Callbacks to be called when no type check is running anymore
|
|
193
|
+
attr_reader :typecheck_quiescent_callbacks
|
|
194
|
+
|
|
189
195
|
def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new, refork: false)
|
|
190
196
|
@project = project
|
|
191
197
|
@reader = reader
|
|
@@ -199,6 +205,11 @@ module Steep
|
|
|
199
205
|
@write_queue = SizedQueue.new(100)
|
|
200
206
|
@refork_mutex = Mutex.new
|
|
201
207
|
@need_to_refork = refork
|
|
208
|
+
@typecheck_quiescent_callbacks = []
|
|
209
|
+
@pending_typecheck_requests = []
|
|
210
|
+
@project_file_mtimes = nil
|
|
211
|
+
@command_socket_requests = {}
|
|
212
|
+
@command_socket_mutex = Mutex.new
|
|
202
213
|
|
|
203
214
|
@controller = TypeCheckController.new(project: project)
|
|
204
215
|
@result_controller = ResultController.new()
|
|
@@ -245,7 +256,7 @@ module Steep
|
|
|
245
256
|
case job.dest
|
|
246
257
|
when :client
|
|
247
258
|
Steep.logger.info { "Processing SendMessageJob: dest=client, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
|
248
|
-
|
|
259
|
+
write_message_to_client(job.message)
|
|
249
260
|
when WorkerProcess
|
|
250
261
|
refork_mutex.synchronize do
|
|
251
262
|
Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
|
|
@@ -420,6 +431,8 @@ module Steep
|
|
|
420
431
|
progress.end()
|
|
421
432
|
end
|
|
422
433
|
|
|
434
|
+
reset_project_file_mtimes()
|
|
435
|
+
|
|
423
436
|
if file_system_watcher_supported?
|
|
424
437
|
setup_file_system_watcher()
|
|
425
438
|
end
|
|
@@ -445,6 +458,7 @@ module Steep
|
|
|
445
458
|
|
|
446
459
|
unless controller.open_paths.include?(path)
|
|
447
460
|
updated_watched_files << path
|
|
461
|
+
record_project_file_mtime(path)
|
|
448
462
|
|
|
449
463
|
case type
|
|
450
464
|
when LSP::Constant::FileChangeType::CREATED, LSP::Constant::FileChangeType::CHANGED
|
|
@@ -684,7 +698,7 @@ module Steep
|
|
|
684
698
|
request.inline_paths << [target_name.to_sym, Pathname(path)]
|
|
685
699
|
end
|
|
686
700
|
|
|
687
|
-
start_type_check(request: request, last_request:
|
|
701
|
+
start_type_check(request: request, last_request: current_type_check_request)
|
|
688
702
|
|
|
689
703
|
when CustomMethods::TypeCheckGroups::METHOD
|
|
690
704
|
params = message[:params] #: CustomMethods::TypeCheckGroups::params
|
|
@@ -703,6 +717,15 @@ module Steep
|
|
|
703
717
|
request.needs_response = false
|
|
704
718
|
start_type_check(request: request, last_request: current_type_check_request, report_progress_threshold: 0)
|
|
705
719
|
|
|
720
|
+
when CustomMethods::Query__Diagnostics::METHOD
|
|
721
|
+
id = message[:id]
|
|
722
|
+
params = message[:params] #: CustomMethods::Query__Diagnostics::params
|
|
723
|
+
paths = params[:paths]
|
|
724
|
+
|
|
725
|
+
run_when_typecheck_quiescent do
|
|
726
|
+
collect_query_diagnostics(id, paths)
|
|
727
|
+
end
|
|
728
|
+
|
|
706
729
|
when "$/ping"
|
|
707
730
|
enqueue_write_job SendMessageJob.to_client(
|
|
708
731
|
message: {
|
|
@@ -805,6 +828,17 @@ module Steep
|
|
|
805
828
|
|
|
806
829
|
def start_type_check(request: nil, last_request:, progress: nil, include_unchanged: false, report_progress_threshold: 10, needs_response: nil)
|
|
807
830
|
Steep.logger.tagged "#start_type_check(#{progress&.guid || request&.guid}, #{last_request&.guid}" do
|
|
831
|
+
if (current = current_type_check_request) && typecheck_request_from_command_socket?(current)
|
|
832
|
+
# Never supersede a type check that a command socket client is waiting for
|
|
833
|
+
if request
|
|
834
|
+
Steep.logger.info { "Queueing type check request #{request.guid} until the command socket request finishes" }
|
|
835
|
+
pending_typecheck_requests << request
|
|
836
|
+
else
|
|
837
|
+
Steep.logger.info { "Deferring automatic type checking until the command socket request finishes" }
|
|
838
|
+
end
|
|
839
|
+
return
|
|
840
|
+
end
|
|
841
|
+
|
|
808
842
|
if last_request
|
|
809
843
|
finish_type_check(last_request)
|
|
810
844
|
end
|
|
@@ -884,6 +918,7 @@ module Steep
|
|
|
884
918
|
finish_type_check(current)
|
|
885
919
|
@current_type_check_request = nil
|
|
886
920
|
refork_workers
|
|
921
|
+
start_pending_typecheck()
|
|
887
922
|
end
|
|
888
923
|
end
|
|
889
924
|
end
|
|
@@ -955,6 +990,17 @@ module Steep
|
|
|
955
990
|
refork_finished.pop
|
|
956
991
|
end
|
|
957
992
|
end
|
|
993
|
+
|
|
994
|
+
# The reforked workers started from a copy of the primary's state, which has the
|
|
995
|
+
# results of the primary's assigned paths only, and the results the replaced workers
|
|
996
|
+
# had computed are gone with them. Type check everything again so that queries
|
|
997
|
+
# reading the stored results, like `$/steep/query/diagnostics`, see all files.
|
|
998
|
+
job_queue << -> do
|
|
999
|
+
guid = SecureRandom.uuid
|
|
1000
|
+
request = controller.make_all_request(guid: guid, progress: work_done_progress(guid))
|
|
1001
|
+
request.needs_response = false
|
|
1002
|
+
start_type_check(request: request, last_request: current_type_check_request, report_progress_threshold: 0)
|
|
1003
|
+
end
|
|
958
1004
|
end
|
|
959
1005
|
end
|
|
960
1006
|
|
|
@@ -1021,6 +1067,341 @@ module Steep
|
|
|
1021
1067
|
write_queue.push(job)
|
|
1022
1068
|
end
|
|
1023
1069
|
|
|
1070
|
+
# Calls the block once no type check is running
|
|
1071
|
+
#
|
|
1072
|
+
# Starts a type check for dirty paths first, so that the block observes up-to-date results.
|
|
1073
|
+
#
|
|
1074
|
+
def run_when_typecheck_quiescent(&block)
|
|
1075
|
+
unless current_type_check_request
|
|
1076
|
+
guid = SecureRandom.uuid
|
|
1077
|
+
if request = controller.make_request(guid: guid, progress: work_done_progress(guid))
|
|
1078
|
+
request.needs_response = false
|
|
1079
|
+
start_type_check(request: request, last_request: nil)
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
if current_type_check_request || !pending_typecheck_requests.empty?
|
|
1084
|
+
typecheck_quiescent_callbacks << block
|
|
1085
|
+
else
|
|
1086
|
+
yield
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
# Starts the next queued type check request if any, or the automatic type check for dirty paths
|
|
1091
|
+
#
|
|
1092
|
+
# Calls the quiescent callbacks when nothing is left to type check.
|
|
1093
|
+
#
|
|
1094
|
+
def start_pending_typecheck
|
|
1095
|
+
while request = pending_typecheck_requests.shift
|
|
1096
|
+
start_type_check(request: request, last_request: nil)
|
|
1097
|
+
return if current_type_check_request
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
if typecheck_automatically && initialize_params
|
|
1101
|
+
guid = SecureRandom.uuid
|
|
1102
|
+
if request = controller.make_request(guid: guid, progress: work_done_progress(guid))
|
|
1103
|
+
request.needs_response = false
|
|
1104
|
+
start_type_check(request: request, last_request: nil)
|
|
1105
|
+
return if current_type_check_request
|
|
1106
|
+
end
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
flush_typecheck_quiescent_callbacks()
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
# Calls the waiting quiescent callbacks, once a type check for the paths that went dirty
|
|
1113
|
+
# in the meantime finishes
|
|
1114
|
+
#
|
|
1115
|
+
def flush_typecheck_quiescent_callbacks
|
|
1116
|
+
return if typecheck_quiescent_callbacks.empty?
|
|
1117
|
+
return if current_type_check_request
|
|
1118
|
+
|
|
1119
|
+
guid = SecureRandom.uuid
|
|
1120
|
+
if request = controller.make_request(guid: guid, progress: work_done_progress(guid))
|
|
1121
|
+
request.needs_response = false
|
|
1122
|
+
start_type_check(request: request, last_request: nil)
|
|
1123
|
+
return if current_type_check_request
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
callbacks = typecheck_quiescent_callbacks.dup
|
|
1127
|
+
typecheck_quiescent_callbacks.clear
|
|
1128
|
+
callbacks.each(&:call)
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
# Collects the stored diagnostics from all typecheck workers and sends the response
|
|
1132
|
+
#
|
|
1133
|
+
# `paths` is an array of absolute path strings to filter the result, or `nil` to return everything.
|
|
1134
|
+
#
|
|
1135
|
+
def collect_query_diagnostics(id, paths)
|
|
1136
|
+
uris = paths&.map {|path| PathHelper.to_uri(Pathname(path)).to_s }
|
|
1137
|
+
|
|
1138
|
+
result_controller << group_request do |group|
|
|
1139
|
+
typecheck_workers.each do |worker|
|
|
1140
|
+
group << send_request(method: CustomMethods::Query__Diagnostics::METHOD, params: nil, worker: worker)
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
group.on_completion do |handlers|
|
|
1144
|
+
diagnostics = {} #: Hash[String, Array[untyped]]
|
|
1145
|
+
|
|
1146
|
+
handlers.each do |handler|
|
|
1147
|
+
result = handler.result or next
|
|
1148
|
+
result.each do |entry|
|
|
1149
|
+
array = diagnostics[entry[:uri]] ||= []
|
|
1150
|
+
array.concat(entry[:diagnostics] || [])
|
|
1151
|
+
array.uniq!
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
# @type var result: CustomMethods::Query__Diagnostics::result
|
|
1156
|
+
result =
|
|
1157
|
+
if uris
|
|
1158
|
+
# Files the server has not type checked yet are reported with `diagnostics: nil`
|
|
1159
|
+
uris.sort.map do |uri|
|
|
1160
|
+
{ uri: uri, diagnostics: diagnostics[uri] }
|
|
1161
|
+
end
|
|
1162
|
+
else
|
|
1163
|
+
diagnostics.keys.sort.map do |uri|
|
|
1164
|
+
{ uri: uri, diagnostics: diagnostics.fetch(uri) }
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
enqueue_write_job SendMessageJob.to_client(
|
|
1169
|
+
message: CustomMethods::Query__Diagnostics.response(id, result)
|
|
1170
|
+
)
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# Methods that command socket clients cannot send because they control the server lifecycle
|
|
1176
|
+
# The only methods a command socket client may send: what `steep check` and
|
|
1177
|
+
# `steep query` use. Everything else is rejected -- lifecycle methods because the
|
|
1178
|
+
# server belongs to whoever started it, and document synchronization because the
|
|
1179
|
+
# LSP client owns the content of the files it opens.
|
|
1180
|
+
COMMAND_SOCKET_ALLOWED_METHODS = [
|
|
1181
|
+
CustomMethods::TypeCheck::METHOD,
|
|
1182
|
+
CustomMethods::Query__Diagnostics::METHOD,
|
|
1183
|
+
CustomMethods::Query__Definition::METHOD,
|
|
1184
|
+
"textDocument/hover",
|
|
1185
|
+
"$/ping"
|
|
1186
|
+
].freeze
|
|
1187
|
+
|
|
1188
|
+
# Notifications that are copied to command socket sessions with a pending `$/steep/typecheck` request
|
|
1189
|
+
COMMAND_SOCKET_FORWARDED_NOTIFICATIONS = ["textDocument/publishDiagnostics", "window/showMessage"].freeze
|
|
1190
|
+
|
|
1191
|
+
# Processes a message received from a command socket client
|
|
1192
|
+
#
|
|
1193
|
+
# Requests are assigned a fresh id and enqueued as if they came from the LSP client,
|
|
1194
|
+
# and the responses are routed back to the session through `#write_message_to_client`.
|
|
1195
|
+
#
|
|
1196
|
+
# May be called from any thread.
|
|
1197
|
+
#
|
|
1198
|
+
def process_command_socket_message(message, session)
|
|
1199
|
+
method = message[:method]
|
|
1200
|
+
id = message[:id]
|
|
1201
|
+
|
|
1202
|
+
if method && !COMMAND_SOCKET_ALLOWED_METHODS.include?(method.to_s)
|
|
1203
|
+
Steep.logger.warn { "Command socket: rejected `#{method}`" }
|
|
1204
|
+
if id
|
|
1205
|
+
session.write({
|
|
1206
|
+
id: id,
|
|
1207
|
+
error: {
|
|
1208
|
+
code: LSP::Constant::ErrorCodes::INVALID_REQUEST,
|
|
1209
|
+
message: "`#{method}` is not allowed through the command socket"
|
|
1210
|
+
}
|
|
1211
|
+
})
|
|
1212
|
+
end
|
|
1213
|
+
return
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
unless initialize_params
|
|
1217
|
+
if id
|
|
1218
|
+
session.write({
|
|
1219
|
+
id: id,
|
|
1220
|
+
error: {
|
|
1221
|
+
code: LSP::Constant::ErrorCodes::SERVER_NOT_INITIALIZED,
|
|
1222
|
+
message: "The language server is not initialized yet"
|
|
1223
|
+
}
|
|
1224
|
+
})
|
|
1225
|
+
end
|
|
1226
|
+
return
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
case
|
|
1230
|
+
when method && id
|
|
1231
|
+
guid = SecureRandom.uuid
|
|
1232
|
+
@command_socket_mutex.synchronize do
|
|
1233
|
+
@command_socket_requests[guid] = [session, id, method.to_s == CustomMethods::TypeCheck::METHOD]
|
|
1234
|
+
end
|
|
1235
|
+
message = message.merge({ id: guid })
|
|
1236
|
+
when id
|
|
1237
|
+
# A response from the client, but nothing is waiting for it
|
|
1238
|
+
return
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
# Reload files changed on disk before processing, because command socket clients
|
|
1242
|
+
# modify files without sending `didChange` notifications
|
|
1243
|
+
job_queue << -> do
|
|
1244
|
+
sync_project_files_from_disk()
|
|
1245
|
+
process_message_from_client(message)
|
|
1246
|
+
end
|
|
1247
|
+
rescue ClosedQueueError
|
|
1248
|
+
Steep.logger.warn { "Command socket: server is shutting down" }
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
# Deregisters the pending requests of a disconnected command socket session
|
|
1252
|
+
def finish_command_socket_session(session)
|
|
1253
|
+
@command_socket_mutex.synchronize do
|
|
1254
|
+
@command_socket_requests.delete_if {|_, entry| entry[0] == session }
|
|
1255
|
+
end
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
def write_message_to_client(message)
|
|
1259
|
+
unless message.key?(:method)
|
|
1260
|
+
entry = @command_socket_mutex.synchronize { @command_socket_requests.delete(message[:id]) }
|
|
1261
|
+
if entry
|
|
1262
|
+
session, original_id, _ = entry
|
|
1263
|
+
session.write(message.merge({ id: original_id }))
|
|
1264
|
+
return
|
|
1265
|
+
end
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
writer.write(message)
|
|
1269
|
+
|
|
1270
|
+
if message.key?(:method) && !message.key?(:id)
|
|
1271
|
+
forward_notification_to_command_sessions(message)
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
def forward_notification_to_command_sessions(message)
|
|
1276
|
+
return unless COMMAND_SOCKET_FORWARDED_NOTIFICATIONS.include?(message[:method].to_s)
|
|
1277
|
+
|
|
1278
|
+
sessions = @command_socket_mutex.synchronize do
|
|
1279
|
+
@command_socket_requests.each_value.with_object([]) do |entry, array|
|
|
1280
|
+
session, _, typecheck = entry
|
|
1281
|
+
array << session if typecheck
|
|
1282
|
+
end
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
sessions.uniq!
|
|
1286
|
+
sessions.each {|session| session.write(message) }
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
def typecheck_request_from_command_socket?(request)
|
|
1290
|
+
@command_socket_mutex.synchronize do
|
|
1291
|
+
@command_socket_requests.key?(request.guid)
|
|
1292
|
+
end
|
|
1293
|
+
end
|
|
1294
|
+
|
|
1295
|
+
# Records the mtimes of all project files, as the baseline of `#sync_project_files_from_disk`
|
|
1296
|
+
def reset_project_file_mtimes
|
|
1297
|
+
mtimes = {} #: Hash[Pathname, Time?]
|
|
1298
|
+
each_project_file_path do |path|
|
|
1299
|
+
mtimes[path] = file_mtime(path)
|
|
1300
|
+
end
|
|
1301
|
+
@project_file_mtimes = mtimes
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def record_project_file_mtime(path)
|
|
1305
|
+
if mtimes = @project_file_mtimes
|
|
1306
|
+
mtimes[path] = file_mtime(path)
|
|
1307
|
+
end
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def file_mtime(path)
|
|
1311
|
+
File.mtime(path)
|
|
1312
|
+
rescue SystemCallError
|
|
1313
|
+
nil
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
def each_project_file_path(&block)
|
|
1317
|
+
loader = Services::FileLoader.new(base_dir: project.base_dir)
|
|
1318
|
+
project.targets.each do |target|
|
|
1319
|
+
loader.each_path_in_target(target) do |relative_path|
|
|
1320
|
+
yield project.absolute_path(relative_path)
|
|
1321
|
+
end
|
|
1322
|
+
end
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# Detects files changed on disk since the last load/sync, and reloads them
|
|
1326
|
+
#
|
|
1327
|
+
# Skips files open in the editor because the client owns their contents.
|
|
1328
|
+
# Does nothing until the initial project load finishes.
|
|
1329
|
+
#
|
|
1330
|
+
def sync_project_files_from_disk
|
|
1331
|
+
mtimes = @project_file_mtimes or return
|
|
1332
|
+
|
|
1333
|
+
changes = {} #: Hash[Pathname, Symbol]
|
|
1334
|
+
seen = Set[] #: Set[Pathname]
|
|
1335
|
+
|
|
1336
|
+
each_project_file_path do |path|
|
|
1337
|
+
next if seen.include?(path)
|
|
1338
|
+
seen << path
|
|
1339
|
+
|
|
1340
|
+
mtime = file_mtime(path)
|
|
1341
|
+
recorded = mtimes[path]
|
|
1342
|
+
|
|
1343
|
+
case
|
|
1344
|
+
when recorded.nil? && mtime
|
|
1345
|
+
changes[path] = mtimes.key?(path) ? :changed : :created
|
|
1346
|
+
when recorded && mtime.nil?
|
|
1347
|
+
changes[path] = :deleted
|
|
1348
|
+
when recorded != mtime
|
|
1349
|
+
changes[path] = :changed
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
mtimes[path] = mtime
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
deleted_paths = mtimes.keys.select {|path| !seen.include?(path) && mtimes[path] && file_mtime(path).nil? }
|
|
1356
|
+
deleted_paths.each do |path|
|
|
1357
|
+
changes[path] = :deleted
|
|
1358
|
+
mtimes[path] = nil
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
changes.delete_if {|path, _| controller.open_paths.include?(path) }
|
|
1362
|
+
return if changes.empty?
|
|
1363
|
+
|
|
1364
|
+
Steep.logger.info { "Command socket: reloading #{changes.size} file(s) changed on disk" }
|
|
1365
|
+
|
|
1366
|
+
changes.each do |path, type|
|
|
1367
|
+
content =
|
|
1368
|
+
if type == :deleted
|
|
1369
|
+
""
|
|
1370
|
+
else
|
|
1371
|
+
begin
|
|
1372
|
+
path.read
|
|
1373
|
+
rescue SystemCallError
|
|
1374
|
+
next
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
case
|
|
1379
|
+
when controller.code_path?(path)
|
|
1380
|
+
controller.add_dirty_code_path(path)
|
|
1381
|
+
when controller.signature_path?(path)
|
|
1382
|
+
controller.add_dirty_signature_path(path)
|
|
1383
|
+
when controller.inline_path?(path)
|
|
1384
|
+
controller.add_dirty_inline_path(path, content)
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
broadcast_notification(CustomMethods::FileReset.notification({ uri: PathHelper.to_uri(path).to_s, content: content }))
|
|
1388
|
+
end
|
|
1389
|
+
|
|
1390
|
+
if typecheck_automatically
|
|
1391
|
+
start_type_checking_queue.execute do
|
|
1392
|
+
job_queue.push(
|
|
1393
|
+
-> do
|
|
1394
|
+
start_type_check(
|
|
1395
|
+
last_request: current_type_check_request,
|
|
1396
|
+
progress: work_done_progress(SecureRandom.uuid),
|
|
1397
|
+
needs_response: false
|
|
1398
|
+
)
|
|
1399
|
+
end
|
|
1400
|
+
)
|
|
1401
|
+
end
|
|
1402
|
+
end
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1024
1405
|
def work_done_progress(guid)
|
|
1025
1406
|
if work_done_progress_supported?
|
|
1026
1407
|
WorkDoneProgress.new(guid) do |message|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
module Steep
|
|
2
2
|
module Server
|
|
3
3
|
class TypeCheckWorker < BaseWorker
|
|
4
|
-
attr_reader :project, :assignment
|
|
4
|
+
attr_reader :project, :assignment
|
|
5
5
|
attr_reader :commandline_args
|
|
6
6
|
attr_reader :current_type_check_guid
|
|
7
7
|
|
|
8
8
|
WorkspaceSymbolJob = _ = Struct.new(:query, :id, keyword_init: true)
|
|
9
9
|
StatsJob = _ = Struct.new(:id, keyword_init: true)
|
|
10
10
|
QueryDefinitionJob = _ = Struct.new(:id, :name, keyword_init: true)
|
|
11
|
+
QueryDiagnosticsJob = _ = Struct.new(:id, keyword_init: true)
|
|
11
12
|
StartTypeCheckJob = _ = Struct.new(:guid, :changes, keyword_init: true)
|
|
12
13
|
TypeCheckCodeJob = _ = Struct.new(:guid, :path, :target, keyword_init: true)
|
|
13
14
|
ValidateAppSignatureJob = _ = Struct.new(:guid, :path, :target, keyword_init: true)
|
|
@@ -68,7 +69,7 @@ module Steep
|
|
|
68
69
|
@io_socket = io_socket
|
|
69
70
|
@service = service if service
|
|
70
71
|
@child_pids = []
|
|
71
|
-
@need_to_warmup =
|
|
72
|
+
@need_to_warmup = true
|
|
72
73
|
|
|
73
74
|
if io_socket
|
|
74
75
|
Signal.trap "SIGCHLD" do
|
|
@@ -112,6 +113,8 @@ module Steep
|
|
|
112
113
|
when CustomMethods::Query__Definition::METHOD
|
|
113
114
|
params = request[:params] #: CustomMethods::Query__Definition::params
|
|
114
115
|
queue << QueryDefinitionJob.new(id: request[:id], name: params[:name])
|
|
116
|
+
when CustomMethods::Query__Diagnostics::METHOD
|
|
117
|
+
queue << QueryDiagnosticsJob.new(id: request[:id])
|
|
115
118
|
when "textDocument/definition"
|
|
116
119
|
queue << GotoJob.definition(id: request[:id], params: request[:params])
|
|
117
120
|
when "textDocument/implementation"
|
|
@@ -307,6 +310,10 @@ module Steep
|
|
|
307
310
|
writer.write(
|
|
308
311
|
CustomMethods::Query__Definition.response(job.id, query_definition_result(job.name))
|
|
309
312
|
)
|
|
313
|
+
when QueryDiagnosticsJob
|
|
314
|
+
writer.write(
|
|
315
|
+
CustomMethods::Query__Diagnostics.response(job.id, query_diagnostics_result())
|
|
316
|
+
)
|
|
310
317
|
end
|
|
311
318
|
end
|
|
312
319
|
|
|
@@ -358,6 +365,45 @@ module Steep
|
|
|
358
365
|
end
|
|
359
366
|
end
|
|
360
367
|
|
|
368
|
+
# Returns the diagnostics of the files this worker has type checked so far
|
|
369
|
+
#
|
|
370
|
+
# An array of `Query__Diagnostics::entry`, with the LSP-formatted diagnostics per file URI.
|
|
371
|
+
# Files that are loaded but not type checked yet are not included.
|
|
372
|
+
#
|
|
373
|
+
def query_diagnostics_result
|
|
374
|
+
result = {} #: Hash[String, Array[untyped]]
|
|
375
|
+
|
|
376
|
+
service.source_files.each_value do |file|
|
|
377
|
+
next if file.typing.nil? && file.errors.nil?
|
|
378
|
+
|
|
379
|
+
absolute_path = project.absolute_path(file.path)
|
|
380
|
+
|
|
381
|
+
group_target =
|
|
382
|
+
project.group_for_source_path(absolute_path) ||
|
|
383
|
+
project.group_for_inline_source_path(absolute_path) ||
|
|
384
|
+
project.target_for_source_path(absolute_path) ||
|
|
385
|
+
project.target_for_inline_source_path(absolute_path)
|
|
386
|
+
next unless group_target
|
|
387
|
+
|
|
388
|
+
formatter = Diagnostic::LSPFormatter.new(group_target.code_diagnostics_config)
|
|
389
|
+
uri = PathHelper.to_uri(absolute_path).to_s
|
|
390
|
+
array = result[uri] ||= []
|
|
391
|
+
array.concat(file.diagnostics.filter_map { formatter.format(_1) })
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
formatter = Diagnostic::LSPFormatter.new({}, **{})
|
|
395
|
+
service.signature_validation_diagnostics.each_value do |path_diagnostics|
|
|
396
|
+
path_diagnostics.each do |path, diagnostics|
|
|
397
|
+
absolute_path = path.absolute? ? path : project.absolute_path(path)
|
|
398
|
+
uri = PathHelper.to_uri(absolute_path).to_s
|
|
399
|
+
array = result[uri] ||= []
|
|
400
|
+
array.concat(diagnostics.filter_map { formatter.format(_1) })
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
result.map { { uri: _1, diagnostics: _2 } }
|
|
405
|
+
end
|
|
406
|
+
|
|
361
407
|
def query_definition_result(name_string)
|
|
362
408
|
name = Services::GotoService.parse_name(name_string)
|
|
363
409
|
|
|
@@ -422,7 +422,7 @@ module Steep
|
|
|
422
422
|
when :send
|
|
423
423
|
location = (_ = node.location) #: Parser::AST::_SelectorLocation
|
|
424
424
|
if test_ast_location(location.selector, line: line, column: column)
|
|
425
|
-
if (parent = parents[0]) && parent.type == :block && parent.children[0] === node
|
|
425
|
+
if (parent = parents[0]) && (parent.type == :block || parent.type == :numblock || parent.type == :itblock) && parent.children[0] === node
|
|
426
426
|
node = parents[0]
|
|
427
427
|
end
|
|
428
428
|
|
|
@@ -524,7 +524,9 @@ module Steep
|
|
|
524
524
|
def constant_definition_in_ruby(name, locations:)
|
|
525
525
|
type_check.source_files.each do |path, source|
|
|
526
526
|
if typing = source.typing
|
|
527
|
-
|
|
527
|
+
# Inline sources are type checked too, but they don't have a Ruby definition to go to.
|
|
528
|
+
# Their declarations are found through the RBS lookup instead.
|
|
529
|
+
target = project.target_for_source_path(path) or next
|
|
528
530
|
entry = typing.source_index.entry(constant: name)
|
|
529
531
|
entry.definitions.each do |node|
|
|
530
532
|
case node.type
|
|
@@ -551,7 +553,9 @@ module Steep
|
|
|
551
553
|
if in_ruby
|
|
552
554
|
type_check.source_files.each do |path, source|
|
|
553
555
|
if typing = source.typing
|
|
554
|
-
|
|
556
|
+
# Inline sources are type checked too, but they don't have a Ruby definition to go to.
|
|
557
|
+
# Their declarations are found through the RBS lookup instead.
|
|
558
|
+
target = project.target_for_source_path(path) or next
|
|
555
559
|
entry = typing.source_index.entry(method: name)
|
|
556
560
|
|
|
557
561
|
if entry.definitions.empty?
|
|
@@ -59,7 +59,7 @@ module Steep
|
|
|
59
59
|
if begin_loc.end_pos <= pos && pos <= end_loc.begin_pos
|
|
60
60
|
# Given position is between open/close parens of args of send node
|
|
61
61
|
|
|
62
|
-
if parent && (parent.type == :block || parent.type == :numblock) && node.equal?(parent.children[0])
|
|
62
|
+
if parent && (parent.type == :block || parent.type == :numblock || parent.type == :itblock) && node.equal?(parent.children[0])
|
|
63
63
|
send_node = parent
|
|
64
64
|
else
|
|
65
65
|
send_node = node
|
data/lib/steep/source.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Steep
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.new_parser
|
|
34
|
-
Prism::Translation::
|
|
34
|
+
Prism::Translation::Parser34.new(Builder.new).tap do |parser|
|
|
35
35
|
parser.diagnostics.all_errors_are_fatal = true
|
|
36
36
|
parser.diagnostics.ignore_warnings = true
|
|
37
37
|
end
|
|
@@ -231,7 +231,7 @@ module Steep
|
|
|
231
231
|
end
|
|
232
232
|
|
|
233
233
|
when :rescue
|
|
234
|
-
body,
|
|
234
|
+
body, _, else_node, loc = deconstruct_rescue_node!(node)
|
|
235
235
|
|
|
236
236
|
if else_node
|
|
237
237
|
loc.else or raise
|
|
@@ -257,7 +257,7 @@ module Steep
|
|
|
257
257
|
annot.line or next
|
|
258
258
|
|
|
259
259
|
case node.type
|
|
260
|
-
when :def, :module, :class, :block, :numblock, :ensure, :defs, :resbody
|
|
260
|
+
when :def, :module, :class, :block, :numblock, :itblock, :ensure, :defs, :resbody
|
|
261
261
|
location = node.loc
|
|
262
262
|
location.line <= annot.line && annot.line < location.last_line
|
|
263
263
|
else
|
|
@@ -356,7 +356,6 @@ module Steep
|
|
|
356
356
|
|
|
357
357
|
def find_heredoc_nodes(line, column, position)
|
|
358
358
|
each_heredoc_node() do |nodes, location|
|
|
359
|
-
node = nodes[0]
|
|
360
359
|
loc = location.heredoc_body #: Parser::Source::Range
|
|
361
360
|
|
|
362
361
|
if range = loc.to_range
|
|
@@ -470,7 +469,7 @@ module Steep
|
|
|
470
469
|
return false unless send_node
|
|
471
470
|
|
|
472
471
|
if send_node.type == :send
|
|
473
|
-
receiver, method,
|
|
472
|
+
receiver, method, _ = deconstruct_send_node!(send_node)
|
|
474
473
|
|
|
475
474
|
return false unless receiver
|
|
476
475
|
|
|
@@ -565,13 +564,13 @@ module Steep
|
|
|
565
564
|
end
|
|
566
565
|
]
|
|
567
566
|
)
|
|
568
|
-
when :numblock
|
|
569
|
-
send,
|
|
567
|
+
when :numblock, :itblock
|
|
568
|
+
send, arg, body = node.children
|
|
570
569
|
node = node.updated(
|
|
571
570
|
nil,
|
|
572
571
|
[
|
|
573
572
|
map_child_node(send) {|child| insert_type_node(child, child_assertions) },
|
|
574
|
-
|
|
573
|
+
arg,
|
|
575
574
|
insert_type_node(body, child_assertions)
|
|
576
575
|
]
|
|
577
576
|
)
|
|
@@ -648,7 +647,7 @@ module Steep
|
|
|
648
647
|
case node.type
|
|
649
648
|
when :send, :csend
|
|
650
649
|
node
|
|
651
|
-
when :block, :numblock
|
|
650
|
+
when :block, :numblock, :itblock
|
|
652
651
|
send = node.children[0]
|
|
653
652
|
case send.type
|
|
654
653
|
when :send, :csend
|
|
@@ -657,7 +656,7 @@ module Steep
|
|
|
657
656
|
end
|
|
658
657
|
|
|
659
658
|
if send_node
|
|
660
|
-
receiver_node,
|
|
659
|
+
receiver_node, _, _, location = deconstruct_send_node!(send_node)
|
|
661
660
|
|
|
662
661
|
if receiver_node
|
|
663
662
|
if location.dot && location.selector
|