[analysis_server] Improve LSP server error messages for invalid paths/parse failures

Change-Id: I1a72ee62e282991b0071d0135c9a658bc5f7e14e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/232861
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index a782782..55257c6 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -208,6 +208,8 @@
   static const InvalidFileLineCol = ErrorCodes(-32004);
   static const UnknownCommand = ErrorCodes(-32005);
   static const InvalidCommandArguments = ErrorCodes(-32006);
+
+  /// A file that is not part of the analyzed set.
   static const FileNotAnalyzed = ErrorCodes(-32007);
   static const FileHasErrors = ErrorCodes(-32008);
   static const ClientFailedToApplyEdit = ErrorCodes(-32009);
@@ -215,6 +217,9 @@
   static const RefactorFailed = ErrorCodes(-32011);
   static const FeatureDisabled = ErrorCodes(-32012);
 
+  /// A file that is expected to be analyzed, but failed.
+  static const FileAnalysisFailed = ErrorCodes(-32013);
+
   /// An error raised when the server detects that the server and client are out
   /// of sync and cannot recover. For example if a textDocument/didChange notification
   /// has invalid offsets, suggesting the client and server have become out of sync
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
index 81b9b85..84da8f2 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_on_type.dart
@@ -23,7 +23,8 @@
   ErrorOr<List<TextEdit>?> formatFile(String path) {
     final file = server.resourceProvider.getFile(path);
     if (!file.exists) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      return error(
+          ServerErrorCodes.InvalidFilePath, 'File does not exist', path);
     }
 
     final result = server.getParsedUnit(path);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
index a6bbf90..f28ef15 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_format_range.dart
@@ -23,7 +23,8 @@
   ErrorOr<List<TextEdit>?> formatRange(String path, Range range) {
     final file = server.resourceProvider.getFile(path);
     if (!file.exists) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      return error(
+          ServerErrorCodes.InvalidFilePath, 'File does not exist', path);
     }
 
     final result = server.getParsedUnit(path);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
index be75ccb..6ff4313 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_formatting.dart
@@ -23,7 +23,8 @@
   ErrorOr<List<TextEdit>?> formatFile(String path) {
     final file = server.resourceProvider.getFile(path);
     if (!file.exists) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      return error(
+          ServerErrorCodes.InvalidFilePath, 'File does not exist', path);
     }
 
     final result = server.getParsedUnit(path);
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
index e0f497c..ddd82dc 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
@@ -56,7 +56,8 @@
     final lineInfo = server.getLineInfo(path);
 
     if (lineInfo == null) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      return error(ServerErrorCodes.InvalidFilePath,
+          'Unable to obtain line information for file', path);
     } else {
       return success(lineInfo);
     }
@@ -65,7 +66,19 @@
   Future<ErrorOr<ResolvedUnitResult>> requireResolvedUnit(String path) async {
     final result = await server.getResolvedUnit(path);
     if (result == null) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      if (server.isAnalyzed(path)) {
+        // If the file was being analyzed and we got a null result, that usually
+        // indicators a parser or analysis failure, so provide a more specific
+        // message.
+        return error(ServerErrorCodes.FileAnalysisFailed,
+            'Analysis failed for file', path);
+      } else {
+        return error(ServerErrorCodes.FileNotAnalyzed,
+            'File is not being analyzed', path);
+      }
+    } else if (!result.exists) {
+      return error(
+          ServerErrorCodes.InvalidFilePath, 'File does not exist', path);
     }
     return success(result);
   }
@@ -73,7 +86,16 @@
   ErrorOr<ParsedUnitResult> requireUnresolvedUnit(String path) {
     final result = server.getParsedUnit(path);
     if (result == null) {
-      return error(ServerErrorCodes.InvalidFilePath, 'Invalid file path', path);
+      if (server.isAnalyzed(path)) {
+        // If the file was being analyzed and we got a null result, that usually
+        // indicators a parser or analysis failure, so provide a more specific
+        // message.
+        return error(ServerErrorCodes.FileAnalysisFailed,
+            'Analysis failed for file', path);
+      } else {
+        return error(ServerErrorCodes.FileNotAnalyzed,
+            'File is not being analyzed', path);
+      }
     }
     return success(result);
   }
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 853fa09..ba6a1d6 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:io';
 import 'dart:math';
 
 import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart'
@@ -798,7 +799,18 @@
     ));
   }
   try {
-    return ErrorOr<String>.success(uri.toFilePath());
+    final filePath = uri.toFilePath();
+    // On Windows, paths that start with \ and not a drive letter are not
+    // supported but will return `true` from `path.isAbsolute` so check for them
+    // specifically.
+    if (Platform.isWindows && filePath.startsWith(r'\')) {
+      return ErrorOr<String>.error(ResponseError(
+        code: lsp.ServerErrorCodes.InvalidFilePath,
+        message: 'URI was not an absolute file path (missing drive letter)',
+        data: uri.toString(),
+      ));
+    }
+    return ErrorOr<String>.success(filePath);
   } catch (e) {
     // Even if tryParse() works and file == scheme, toFilePath() can throw on
     // Windows if there are invalid characters.
diff --git a/pkg/analysis_server/test/lsp/document_symbols_test.dart b/pkg/analysis_server/test/lsp/document_symbols_test.dart
index a37882a..48ddc1b 100644
--- a/pkg/analysis_server/test/lsp/document_symbols_test.dart
+++ b/pkg/analysis_server/test/lsp/document_symbols_test.dart
@@ -206,8 +206,10 @@
     newFile(mainFilePath, content: content);
     await initialize(allowEmptyRootUri: true);
 
-    await expectLater(getDocumentSymbols(mainFileUri.toString()),
-        throwsA(isResponseError(ServerErrorCodes.InvalidFilePath)));
+    await expectLater(
+        getDocumentSymbols(mainFileUri.toString()),
+        throwsA(isResponseError(ServerErrorCodes.FileNotAnalyzed,
+            message: 'File is not being analyzed')));
   }
 
   Future<void> test_nonDartFile() async {
diff --git a/pkg/analysis_server/test/lsp/format_test.dart b/pkg/analysis_server/test/lsp/format_test.dart
index 7ffff8a..f3d24e8 100644
--- a/pkg/analysis_server/test/lsp/format_test.dart
+++ b/pkg/analysis_server/test/lsp/format_test.dart
@@ -640,7 +640,8 @@
     await expectLater(
       formatDocument(
           Uri.file(join(projectFolderPath, 'missing.dart')).toString()),
-      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath)),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'File does not exist')),
     );
   }
 
@@ -649,8 +650,9 @@
 
     await expectLater(
       // Add some invalid path characters to the end of a valid file:// URI.
-      formatDocument(mainFileUri.toString() + '***.dart'),
-      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath)),
+      formatDocument(mainFileUri.toString() + r'***###\\\///:::.dart'),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'File URI did not contain a valid file path')),
     );
   }
 
@@ -659,7 +661,8 @@
 
     await expectLater(
       formatDocument('a:/a.dart'),
-      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath)),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'URI was not a valid file:// URI')),
     );
   }
 
diff --git a/pkg/analysis_server/test/lsp/server_test.dart b/pkg/analysis_server/test/lsp/server_test.dart
index 22608c0..48d0109 100644
--- a/pkg/analysis_server/test/lsp/server_test.dart
+++ b/pkg/analysis_server/test/lsp/server_test.dart
@@ -2,11 +2,15 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:io';
+
 import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
 import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
+import '../tool/lsp_spec/matchers.dart';
 import 'server_abstract.dart';
 
 void main() {
@@ -44,6 +48,67 @@
     await server.exited.timeout(const Duration(seconds: 10));
   }
 
+  Future<void> test_path_doesNotExist() async {
+    final missingFileUri = Uri.file(join(projectFolderPath, 'missing.dart'));
+    await initialize();
+    await expectLater(
+      getHover(missingFileUri, startOfDocPos),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'File does not exist')),
+    );
+  }
+
+  Future<void> test_path_invalidFormat() async {
+    await initialize();
+    await expectLater(
+      // Add some invalid path characters to the end of a valid file:// URI.
+      formatDocument(mainFileUri.toString() + r'***###\\\///:::.dart'),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'File URI did not contain a valid file path')),
+    );
+  }
+
+  Future<void> test_path_missingDriveLetterWindows() async {
+    // This test is only valid on Windows, as a URI in the format:
+    //    file:///foo/bar.dart
+    // is valid for non-Windows platforms, but not valid on Windows as it does
+    // not have a drive letter.
+    if (!Platform.isWindows) {
+      return;
+    }
+    final missingDriveLetterFileUri = Uri.file('/foo/bar.dart');
+    await initialize();
+    await expectLater(
+      getHover(missingDriveLetterFileUri, startOfDocPos),
+      // The Uri.file() above translates to a non-file:// URI of just 'a/b.dart'
+      // so will get the not-file-scheme error message.
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'URI was not an absolute file path (missing drive letter)')),
+    );
+  }
+
+  Future<void> test_path_notFileScheme() async {
+    final relativeFileUri = Uri(scheme: 'foo', path: '/a/b.dart');
+    await initialize();
+    await expectLater(
+      getHover(relativeFileUri, startOfDocPos),
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'URI was not a valid file:// URI')),
+    );
+  }
+
+  Future<void> test_path_relative() async {
+    final relativeFileUri = Uri.file('a/b.dart');
+    await initialize();
+    await expectLater(
+      getHover(relativeFileUri, startOfDocPos),
+      // The Uri.file() above translates to a non-file:// URI of just 'a/b.dart'
+      // so will get the not-file-scheme error message.
+      throwsA(isResponseError(ServerErrorCodes.InvalidFilePath,
+          message: 'URI was not a valid file:// URI')),
+    );
+  }
+
   Future<void> test_shutdown_initialized() async {
     await initialize();
     final response = await sendShutdown();
diff --git a/pkg/analysis_server/test/tool/lsp_spec/matchers.dart b/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
index a424ae1..65bb7ae 100644
--- a/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
+++ b/pkg/analysis_server/test/tool/lsp_spec/matchers.dart
@@ -15,8 +15,13 @@
 Matcher isMapOf(Matcher indexMatcher, Matcher valueMatcher) =>
     MapTypeMatcher(wrapMatcher(indexMatcher), wrapMatcher(valueMatcher));
 
-Matcher isResponseError(ErrorCodes code) => const TypeMatcher<ResponseError>()
-    .having((e) => e.code, 'code', equals(code));
+Matcher isResponseError(ErrorCodes code, {String? message}) {
+  final matcher = const TypeMatcher<ResponseError>()
+      .having((e) => e.code, 'code', equals(code));
+  return message != null
+      ? matcher.having((e) => e.message, 'message', equals(message))
+      : matcher;
+}
 
 Matcher isSimpleType(String name) => SimpleTypeMatcher(name);