Skip to content

Commit 1ea71b4

Browse files
DanTupCommit Queue
authored and
Commit Queue
committed
[analysis_server] Handle unexpected types in LSP experimental client capabilities LSP gracefully
Previously if you sent values in experimental capabilities that are different types to what we'd expect, in some cases we'd throw an exception and fail to initialize the server. Since the experimental section is not specified (it's specifically for off-spec things) the behaviour of sending values should (IMO) be considered server-specific. However, reporting a useful message and continuing to function is clearly better than reporting a cryptic error and failing to initialize. At least one client (`kak-lsp`) appears to be sending values in `"commands"` intended for the Rust LSP server which uses a different format to we're using (a nested Map where we use a List). This change will prevent the failure for them and instead report a warning. My recommendation is that their client is also updated to not send Rust experimental capabilities to non-Rust servers. Fixes #55935 Change-Id: I8cba1aa3a5beebf884ff247180f809ffd33b3111 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/371400 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Sam Rawlins <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 1a45349 commit 1ea71b4

File tree

4 files changed

+232
-35
lines changed

4 files changed

+232
-35
lines changed

pkg/analysis_server/lib/src/lsp/client_capabilities.dart

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ class LspClientCapabilities {
118118
/// A set of commands that exist on the client that the server may call.
119119
final Set<String> supportedCommands;
120120

121+
/// User-friendly error messages from parsing the experimental capabilities.
122+
final List<String> experimentalCapabilitiesErrors;
123+
121124
factory LspClientCapabilities(ClientCapabilities raw) {
122125
var workspace = raw.workspace;
123126
var workspaceEdit = workspace?.workspaceEdit;
@@ -137,8 +140,6 @@ class LspClientCapabilities {
137140
var definition = textDocument?.definition;
138141
var typeDefinition = textDocument?.typeDefinition;
139142
var workspaceSymbol = workspace?.symbol;
140-
var experimental = _mapOrEmpty(raw.experimental);
141-
var experimentalActions = _mapOrEmpty(experimental['dartCodeAction']);
142143

143144
var applyEdit = workspace?.applyEdit ?? false;
144145
var codeActionKinds =
@@ -184,30 +185,8 @@ class LspClientCapabilities {
184185
var workDoneProgress = raw.window?.workDoneProgress ?? false;
185186
var workspaceSymbolKinds = _listToSet(workspaceSymbol?.symbolKind?.valueSet,
186187
defaults: defaultSupportedSymbolKinds);
187-
var experimentalSnippetTextEdit = experimental['snippetTextEdit'] == true;
188-
var commandParameterSupport =
189-
_mapOrEmpty(experimentalActions['commandParameterSupport']);
190-
var commandParameterSupportedKinds =
191-
_listToSet(commandParameterSupport['supportedKinds'] as List?)
192-
.cast<String>();
193-
var supportsDartExperimentalTextDocumentContentProvider =
194-
(experimental[dartExperimentalTextDocumentContentProviderKey] ??
195-
experimental[
196-
dartExperimentalTextDocumentContentProviderLegacyKey]) !=
197-
null;
198-
var supportedCommands =
199-
_listToSet(experimental['commands'] as List?).cast<String>();
200188

201-
/// At the time of writing (2023-02-01) there is no official capability for
202-
/// supporting 'showMessageRequest' because LSP assumed all clients
203-
/// supported it.
204-
///
205-
/// This turned out to not be the case, so to avoid sending prompts that
206-
/// might not be seen, we will only use this functionality if we _know_ the
207-
/// client supports it via a custom flag in 'experimental' that is passed by
208-
/// the Dart-Code VS Code extension since version v3.58.0 (2023-01-25).
209-
var supportsShowMessageRequest =
210-
experimental['supportsWindowShowMessageRequest'] == true;
189+
var experimental = _ExperimentalClientCapabilities.parse(raw.experimental);
211190

212191
return LspClientCapabilities._(
213192
raw,
@@ -241,12 +220,14 @@ class LspClientCapabilities {
241220
completionLabelDetails: completionLabelDetails,
242221
completionDefaultEditRange: completionDefaultEditRange,
243222
completionDefaultTextMode: completionDefaultTextMode,
244-
experimentalSnippetTextEdit: experimentalSnippetTextEdit,
245-
codeActionCommandParameterSupportedKinds: commandParameterSupportedKinds,
246-
supportsShowMessageRequest: supportsShowMessageRequest,
223+
experimentalSnippetTextEdit: experimental.snippetTextEdit,
224+
codeActionCommandParameterSupportedKinds:
225+
experimental.commandParameterKinds,
226+
supportsShowMessageRequest: experimental.showMessageRequest,
247227
supportsDartExperimentalTextDocumentContentProvider:
248-
supportsDartExperimentalTextDocumentContentProvider,
249-
supportedCommands: supportedCommands,
228+
experimental.dartTextDocumentContentProvider,
229+
supportedCommands: experimental.commands,
230+
experimentalCapabilitiesErrors: experimental.errors,
250231
);
251232
}
252233

@@ -287,6 +268,7 @@ class LspClientCapabilities {
287268
required this.supportsShowMessageRequest,
288269
required this.supportsDartExperimentalTextDocumentContentProvider,
289270
required this.supportedCommands,
271+
required this.experimentalCapabilitiesErrors,
290272
});
291273

292274
/// Converts a list to a `Set`, returning null if the list is null.
@@ -300,8 +282,120 @@ class LspClientCapabilities {
300282
static Set<T> _listToSet<T>(List<T>? items, {Set<T> defaults = const {}}) {
301283
return items != null ? {...items} : defaults;
302284
}
285+
}
286+
287+
/// A helper for parsing experimental capabilities and collecting any errors
288+
/// because their values do not match the types expected by the server.
289+
class _ExperimentalClientCapabilities {
290+
/// User-friendly error messages from parsing the experimental capabilities.
291+
final List<String> errors;
292+
293+
final bool snippetTextEdit;
294+
final Set<String> commandParameterKinds;
295+
final bool dartTextDocumentContentProvider;
296+
final Set<String> commands;
297+
final bool showMessageRequest;
298+
299+
_ExperimentalClientCapabilities({
300+
required this.snippetTextEdit,
301+
required this.commandParameterKinds,
302+
required this.dartTextDocumentContentProvider,
303+
required this.commands,
304+
required this.showMessageRequest,
305+
required this.errors,
306+
});
307+
308+
/// Parse the experimental capabilities.
309+
///
310+
/// Unlike the capabilities above the spec doesn't define any types for
311+
/// these, so we may see types we don't expect (whereas the above would have
312+
/// failed to deserialize if the types are invalid). So, check the types
313+
/// carefully and report a warning to the client if something looks wrong.
314+
///
315+
/// Example: https://github.com/dart-lang/sdk/issues/55935
316+
factory _ExperimentalClientCapabilities.parse(Object? raw) {
317+
var errors = <String>[];
318+
319+
/// Helper to ensure [object] is type [T] and otherwise records an error in
320+
/// [errors] and returns `null`.
321+
T? expectType<T>(String suffix, Object? object, [String? typeDescription]) {
322+
if (object is! T) {
323+
errors.add(
324+
'ClientCapabilities.experimental$suffix must be a ${typeDescription ?? T}');
325+
return null;
326+
}
327+
return object;
328+
}
329+
330+
var expectMap = expectType<Map<String, Object?>?>;
331+
var expectBool = expectType<bool?>;
332+
var expectString = expectType<String>;
333+
334+
/// Helper to expect a nullable list of strings and return them as a set.
335+
Set<String>? expectNullableStringSet(String name, Object? object) {
336+
return expectType<List<Object?>?>(name, object, 'List<String>?')
337+
?.map((item) => expectString('$name[]', item))
338+
.nonNulls
339+
.toSet();
340+
}
341+
342+
var experimental = expectMap('', raw) ?? const {};
303343

304-
static Map<String, Object?> _mapOrEmpty(Object? item) {
305-
return item is Map<String, Object?> ? item : const {};
344+
// Snippets.
345+
var snippetTextEdit = expectBool(
346+
'.snippetTextEdit',
347+
experimental['snippetTextEdit'],
348+
);
349+
350+
// Refactor command parameters.
351+
var experimentalActions = expectMap(
352+
'.dartCodeAction',
353+
experimental['dartCodeAction'],
354+
);
355+
experimentalActions ??= const {};
356+
var commandParameters = expectMap(
357+
'.dartCodeAction.commandParameterSupport',
358+
experimentalActions['commandParameterSupport'],
359+
);
360+
commandParameters ??= {};
361+
var commandParameterKinds = expectNullableStringSet(
362+
'.dartCodeAction.commandParameterSupport.supportedKinds',
363+
commandParameters['supportedKinds'],
364+
);
365+
366+
// Macro/Augmentation content.
367+
var dartContentValue =
368+
experimental[dartExperimentalTextDocumentContentProviderKey] ??
369+
experimental[dartExperimentalTextDocumentContentProviderLegacyKey];
370+
var dartTextDocumentContentProvider = expectBool(
371+
'.$dartExperimentalTextDocumentContentProviderKey',
372+
dartContentValue,
373+
);
374+
375+
// Executable commands.
376+
var commands =
377+
expectNullableStringSet('.commands', experimental['commands']);
378+
379+
/// At the time of writing (2023-02-01) there is no official capability for
380+
/// supporting 'showMessageRequest' because LSP assumed all clients
381+
/// supported it.
382+
///
383+
/// This turned out to not be the case, so to avoid sending prompts that
384+
/// might not be seen, we will only use this functionality if we _know_ the
385+
/// client supports it via a custom flag in 'experimental' that is passed by
386+
/// the Dart-Code VS Code extension since version v3.58.0 (2023-01-25).
387+
var showMessageRequest = expectBool(
388+
'.supportsWindowShowMessageRequest',
389+
experimental['supportsWindowShowMessageRequest'],
390+
);
391+
392+
return _ExperimentalClientCapabilities(
393+
snippetTextEdit: snippetTextEdit ?? false,
394+
commandParameterKinds: commandParameterKinds ?? {},
395+
dartTextDocumentContentProvider: dartTextDocumentContentProvider ?? false,
396+
commands: commands ?? {},
397+
showMessageRequest: showMessageRequest ?? false,
398+
errors: errors,
399+
);
306400
}
307401
}

pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,22 @@ class LspAnalysisServer extends AnalysisServer {
396396

397397
_checkAnalytics();
398398
enableSurveys();
399+
400+
// Notify the client of any issues parsing experimental capabilities.
401+
var errors = _clientCapabilities?.experimentalCapabilitiesErrors;
402+
if (errors != null && errors.isNotEmpty) {
403+
var message = 'There were errors parsing your experimental '
404+
'client capabilities:\n${errors.join(', ')}';
405+
406+
var params =
407+
ShowMessageParams(type: MessageType.warning.forLsp, message: message);
408+
409+
channel.sendNotification(NotificationMessage(
410+
method: Method.window_showMessage,
411+
params: params,
412+
jsonrpc: jsonRpcVersion,
413+
));
414+
}
399415
}
400416

401417
/// Handles a response from the client by invoking the completer that the

pkg/analysis_server/test/lsp/initialization_test.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import 'dart:async';
66

77
import 'package:analysis_server/lsp_protocol/protocol.dart';
8-
import 'package:analysis_server/src/analysis_server.dart';
8+
import 'package:analysis_server/src/analysis_server.dart' hide MessageType;
99
import 'package:analysis_server/src/lsp/constants.dart';
1010
import 'package:analysis_server/src/lsp/server_capabilities_computer.dart';
1111
import 'package:analysis_server/src/plugin/plugin_manager.dart';
@@ -57,6 +57,27 @@ class InitializationTest extends AbstractLspAnalysisServerTest {
5757
expect(registeredMethods, equals(result));
5858
}
5959

60+
/// Expects successful initialization, but a window/showMessage warning
61+
/// [expectedMessage] for the given [experimentalCapabilities].
62+
Future<void> expectInvalidExperimentalParams(
63+
Map<String, Object?> experimentalCapabilities,
64+
String expectedMessage,
65+
) async {
66+
var warning = showMessageNotifications
67+
.firstWhere((m) => m.type == MessageType.Warning);
68+
69+
await initialize(experimentalCapabilities: experimentalCapabilities);
70+
var message = (await warning).message;
71+
72+
expect(
73+
message,
74+
allOf(
75+
contains('errors parsing your experimental client capabilities'),
76+
contains(expectedMessage),
77+
),
78+
);
79+
}
80+
6081
TextDocumentRegistrationOptions registrationOptionsFor(
6182
List<Registration> registrations,
6283
Method method,
@@ -936,6 +957,57 @@ class InitializationTest extends AbstractLspAnalysisServerTest {
936957
expect(server.contextManager.includedPaths, equals([projectFolderPath]));
937958
}
938959

960+
Future<void> test_invalidExperimental_commands() async {
961+
await expectInvalidExperimentalParams(
962+
{'commands': 1},
963+
'ClientCapabilities.experimental.commands must be a List<String>?',
964+
);
965+
}
966+
967+
Future<void> test_invalidExperimental_dartCodeAction() async {
968+
await expectInvalidExperimentalParams(
969+
{'dartCodeAction': 1},
970+
'ClientCapabilities.experimental.dartCodeAction must be a Map<String, Object?>?',
971+
);
972+
}
973+
974+
Future<void>
975+
test_invalidExperimental_dartCodeAction_commandParameterSupport() async {
976+
await expectInvalidExperimentalParams(
977+
{
978+
'dartCodeAction': {'commandParameterSupport': 1}
979+
},
980+
'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport must be a Map<String, Object?>?',
981+
);
982+
}
983+
984+
Future<void>
985+
test_invalidExperimental_dartCodeAction_commandParameterSupport_supportedKinds() async {
986+
await expectInvalidExperimentalParams(
987+
{
988+
'dartCodeAction': {
989+
'commandParameterSupport': {'supportedKinds': 1}
990+
}
991+
},
992+
'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport.supportedKinds must be a List<String>?',
993+
);
994+
}
995+
996+
Future<void> test_invalidExperimental_snippetTextEdit() async {
997+
await expectInvalidExperimentalParams(
998+
{'snippetTextEdit': 1},
999+
'ClientCapabilities.experimental.snippetTextEdit must be a bool?',
1000+
);
1001+
}
1002+
1003+
Future<void>
1004+
test_invalidExperimental_supportsDartTextDocumentContentProvider() async {
1005+
await expectInvalidExperimentalParams(
1006+
{'supportsDartTextDocumentContentProvider': 1},
1007+
'ClientCapabilities.experimental.supportsDartTextDocumentContentProvider must be a bool?',
1008+
);
1009+
}
1010+
9391011
Future<void> test_nonFileScheme_rootUri() async {
9401012
// We expect an error notification about the invalid file we try to open.
9411013
failTestOnAnyErrorNotification = false;

pkg/analysis_server/test/lsp/server_abstract.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,14 @@ mixin LspAnalysisServerTestMixin
903903

904904
Stream<Message> get serverToClient;
905905

906+
/// A stream of [ShowMessageParams] for any `window/logMessage` notifications.
907+
Stream<ShowMessageParams> get showMessageNotifications =>
908+
notificationsFromServer
909+
.where((notification) =>
910+
notification.method == Method.window_showMessage)
911+
.map((message) => ShowMessageParams.fromJson(
912+
message.params as Map<String, Object?>));
913+
906914
String get testPackageRootPath => projectFolderPath;
907915

908916
Future<void> changeFile(
@@ -1598,7 +1606,14 @@ mixin LspAnalysisServerTestMixin
15981606
/// ensure no errors come from the server in response to notifications (which
15991607
/// don't have their own responses).
16001608
bool _isErrorNotification(NotificationMessage notification) {
1601-
return notification.method == Method.window_logMessage ||
1602-
notification.method == Method.window_showMessage;
1609+
var method = notification.method;
1610+
var params = notification.params as Map<String, Object?>?;
1611+
if (method == Method.window_logMessage && params != null) {
1612+
return LogMessageParams.fromJson(params).type == MessageType.Error;
1613+
} else if (method == Method.window_showMessage && params != null) {
1614+
return ShowMessageParams.fromJson(params).type == MessageType.Error;
1615+
} else {
1616+
return false;
1617+
}
16031618
}
16041619
}

0 commit comments

Comments
 (0)