[analysis_server] Use relative imports for completion imports when prefer_relative_imports lint is enabled

Change-Id: Ib72066102db1883a21cf7a42e18a81efcfb47c54
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/220200
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
index 30935d6..72903a8 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
@@ -60,11 +60,12 @@
           'Requests not before server is initilized');
     }
 
-    final lineInfo = server.getLineInfo(data.file);
+    final file = data.file;
+    final lineInfo = server.getLineInfo(file);
     if (lineInfo == null) {
       return error(
         ErrorCodes.InternalError,
-        'Line info not available for ${data.file}',
+        'Line info not available for $file',
         null,
       );
     }
@@ -99,7 +100,7 @@
     _latestCompletionItem = item;
     while (item == _latestCompletionItem && timer.elapsed < timeout) {
       try {
-        final analysisDriver = server.getAnalysisDriver(data.file);
+        final analysisDriver = server.getAnalysisDriver(file);
         final session = analysisDriver?.currentSession;
 
         // We shouldn't not get a driver/session, but if we did perhaps the file
@@ -139,7 +140,7 @@
 
         var newInsertText = item.insertText ?? item.label;
         final builder = ChangeBuilder(session: session);
-        await builder.addDartFileEdit(data.file, (builder) {
+        await builder.addDartFileEdit(file, (builder) {
           final result = builder.importLibraryElement(library.uri);
           if (result.prefix != null) {
             newInsertText = '${result.prefix}.$newInsertText';
@@ -152,9 +153,9 @@
 
         final changes = builder.sourceChange;
         final thisFilesChanges =
-            changes.edits.where((e) => e.file == data.file).toList();
+            changes.edits.where((e) => e.file == file).toList();
         final otherFilesChanges =
-            changes.edits.where((e) => e.file != data.file).toList();
+            changes.edits.where((e) => e.file != file).toList();
 
         // If this completion involves editing other files, we'll need to build
         // a command that the client will call to apply those edits later.
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart b/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
index 415f155..4a949c4 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
@@ -16,6 +16,7 @@
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/source/source_range.dart';
 import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_dart.dart';
+import 'package:analyzer_plugin/src/utilities/library.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
 import 'package:analyzer_plugin/utilities/range_factory.dart';
@@ -150,22 +151,6 @@
     return false;
   }
 
-  /// Returns the relative URI from the passed [library] to the given [path].
-  ///
-  /// If the [path] is not in the [library]'s directory, `null` is returned.
-  String? _getRelativeUriFromLibrary(LibraryElement library, String path) {
-    var librarySource = library.librarySource;
-    var pathContext = resourceProvider.pathContext;
-    var libraryDirectory = pathContext.dirname(librarySource.fullName);
-    var sourceDirectory = pathContext.dirname(path);
-    if (pathContext.isWithin(libraryDirectory, path) ||
-        pathContext.isWithin(sourceDirectory, libraryDirectory)) {
-      var relativeFile = pathContext.relative(path, from: libraryDirectory);
-      return pathContext.split(relativeFile).join('/');
-    }
-    return null;
-  }
-
   Iterable<CorrectionProducer> _importExtensionInLibrary(
       Uri uri, DartType targetType, String memberName) sync* {
     // Look to see whether the library at the [uri] is already imported. If it
@@ -207,25 +192,28 @@
 
   /// Returns a list of one or two import corrections.
   ///
-  /// If [relativeUri] is `null`, only one correction, with an absolute import
-  /// path, is returned. Otherwise, a correction with an absolute import path
-  /// and a correction with a relative path are returned. If the
+  /// If [includeRelativeFix] is `false`, only one correction, with an absolute
+  /// import path, is returned. Otherwise, a correction with an absolute import
+  /// path and a correction with a relative path are returned. If the
   /// `prefer_relative_imports` lint rule is enabled, the relative path is
   /// returned first.
-  Iterable<CorrectionProducer> _importLibrary(FixKind fixKind, Uri library,
-      [String? relativeUri]) {
-    if (relativeUri == null || relativeUri.isEmpty) {
+  Iterable<CorrectionProducer> _importLibrary(
+    FixKind fixKind,
+    Uri library, {
+    bool includeRelativeFix = false,
+  }) {
+    if (!includeRelativeFix) {
       return [_ImportAbsoluteLibrary(fixKind, library)];
     }
     if (isLintEnabled(LintNames.prefer_relative_imports)) {
       return [
-        _ImportRelativeLibrary(fixKind, relativeUri),
+        _ImportRelativeLibrary(fixKind, library),
         _ImportAbsoluteLibrary(fixKind, library),
       ];
     } else {
       return [
         _ImportAbsoluteLibrary(fixKind, library),
-        _ImportRelativeLibrary(fixKind, relativeUri),
+        _ImportRelativeLibrary(fixKind, library),
       ];
     }
   }
@@ -312,10 +300,13 @@
         // Good: direct declaration.
         fixKind = DartFixKind.IMPORT_LIBRARY_PROJECT1;
       }
-      // Add the fix.
-      var relativeUri =
-          _getRelativeUriFromLibrary(libraryElement, declaration.path);
-      yield* _importLibrary(fixKind, declaration.uri, relativeUri);
+      // If both files are in the same package's lib folder, also include a
+      // relative import.
+      var includeRelativeUri = canBeRelativeImport(
+          declaration.uri, libraryElement.librarySource.uri);
+      // Add the fix(es).
+      yield* _importLibrary(fixKind, declaration.uri,
+          includeRelativeFix: includeRelativeUri);
     }
   }
 
@@ -415,7 +406,9 @@
   @override
   Future<void> compute(ChangeBuilder builder) async {
     await builder.addDartFileEdit(file, (builder) {
-      _uriText = builder.importLibrary(_library);
+      if (builder is DartFileEditBuilderImpl) {
+        _uriText = builder.importLibraryWithAbsoluteUri(_library);
+      }
     });
   }
 }
@@ -532,12 +525,14 @@
 class _ImportRelativeLibrary extends CorrectionProducer {
   final FixKind _fixKind;
 
-  final String _relativeURI;
+  final Uri _library;
 
-  _ImportRelativeLibrary(this._fixKind, this._relativeURI);
+  String _uriText = '';
+
+  _ImportRelativeLibrary(this._fixKind, this._library);
 
   @override
-  List<Object> get fixArguments => [_relativeURI];
+  List<Object> get fixArguments => [_uriText];
 
   @override
   FixKind get fixKind => _fixKind;
@@ -546,7 +541,7 @@
   Future<void> compute(ChangeBuilder builder) async {
     await builder.addDartFileEdit(file, (builder) {
       if (builder is DartFileEditBuilderImpl) {
-        builder.importLibraryWithRelativeUri(_relativeURI);
+        _uriText = builder.importLibraryWithRelativeUri(_library);
       }
     });
   }
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index c2e57dd..cbacc55 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -4,9 +4,11 @@
 
 import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
 import 'package:analysis_server/src/lsp/constants.dart';
+import 'package:analysis_server/src/services/linter/lint_names.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 import 'package:collection/collection.dart';
+import 'package:linter/src/rules.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
@@ -1621,6 +1623,36 @@
     expect(resolved.detail, isNull);
   }
 
+  Future<void> test_suggestionSets_importsPackageUri() async {
+    newFile(
+      join(projectFolderPath, 'lib', 'my_class.dart'),
+      content: 'class MyClass {}',
+    );
+
+    final content = '''
+void f() {
+  MyClas^
+}
+    ''';
+
+    final expectedContent = '''
+import 'package:test/my_class.dart';
+
+void f() {
+  MyClass
+}
+    ''';
+
+    final completionLabel = 'MyClass';
+
+    await _checkCompletionEdits(
+      mainFileUri,
+      content,
+      completionLabel,
+      expectedContent,
+    );
+  }
+
   Future<void>
       test_suggestionSets_includesReexportedSymbolsForEachFile() async {
     newFile(
@@ -2018,6 +2050,78 @@
     '''));
   }
 
+  Future<void> test_suggestionSets_preferRelativeImportsLib_insideLib() async {
+    _enableLints([LintNames.prefer_relative_imports]);
+    final importingFilePath =
+        join(projectFolderPath, 'lib', 'nested1', 'main.dart');
+    final importingFileUri = Uri.file(importingFilePath);
+    final importedFilePath =
+        join(projectFolderPath, 'lib', 'nested2', 'imported.dart');
+
+    // Create a file that will be auto-imported from completion.
+    newFile(importedFilePath, content: 'class MyClass {}');
+
+    final content = '''
+void f() {
+  MyClas^
+}
+    ''';
+
+    final expectedContent = '''
+import '../nested2/imported.dart';
+
+void f() {
+  MyClass
+}
+    ''';
+
+    final completionLabel = 'MyClass';
+
+    await _checkCompletionEdits(
+      importingFileUri,
+      content,
+      completionLabel,
+      expectedContent,
+    );
+  }
+
+  Future<void> test_suggestionSets_preferRelativeImportsLib_outsideLib() async {
+    // Files outside of the lib folder should still get absolute imports to
+    // files inside lib, even with the lint enabled.
+    _enableLints([LintNames.prefer_relative_imports]);
+    final importingFilePath =
+        join(projectFolderPath, 'bin', 'nested1', 'main.dart');
+    final importingFileUri = Uri.file(importingFilePath);
+    final importedFilePath =
+        join(projectFolderPath, 'lib', 'nested2', 'imported.dart');
+
+    // Create a file that will be auto-imported from completion.
+    newFile(importedFilePath, content: 'class MyClass {}');
+
+    final content = '''
+void f() {
+  MyClas^
+}
+    ''';
+
+    final expectedContent = '''
+import 'package:test/nested2/imported.dart';
+
+void f() {
+  MyClass
+}
+    ''';
+
+    final completionLabel = 'MyClass';
+
+    await _checkCompletionEdits(
+      importingFileUri,
+      content,
+      completionLabel,
+      expectedContent,
+    );
+  }
+
   Future<void> test_suggestionSets_unavailableIfDisabled() async {
     newFile(
       join(projectFolderPath, 'other_file.dart'),
@@ -2100,6 +2204,39 @@
     expect(updated, contains('a.abcdefghij'));
   }
 
+  /// Sets up the server with a file containing [content] and checks that
+  /// accepting a specific completion produces [expectedContent].
+  ///
+  /// [content] should contain a `^` at the location where completion should be
+  /// invoked/accepted.
+  Future<void> _checkCompletionEdits(
+    Uri fileUri,
+    String content,
+    String completionLabel,
+    String expectedContent,
+  ) async {
+    final initialAnalysis = waitForAnalysisComplete();
+    await initialize(
+        workspaceCapabilities:
+            withApplyEditSupport(emptyWorkspaceClientCapabilities));
+    await openFile(fileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(fileUri, positionFromMarker(content));
+
+    final completion = res.where((c) => c.label == completionLabel).single;
+    final resolvedCompletion = await resolveCompletion(completion);
+
+    // Apply both the main completion edit and the additionalTextEdits atomically.
+    final newContent = applyTextEdits(
+      withoutMarkers(content),
+      [toTextEdit(resolvedCompletion.textEdit!)]
+          .followedBy(resolvedCompletion.additionalTextEdits!)
+          .toList(),
+    );
+
+    expect(newContent, equals(expectedContent));
+  }
+
   Future<void> _checkResultsForTriggerCharacters(String content,
       List<String> triggerCharacters, Matcher expectedResults) async {
     await initialize();
@@ -2114,6 +2251,16 @@
       expect(res, expectedResults);
     }
   }
+
+  void _enableLints(List<String> lintNames) {
+    registerLintRules();
+    final lintsYaml = lintNames.map((name) => '    - $name\n').join();
+    newFile(analysisOptionsPath, content: '''
+linter:
+  rules:
+$lintsYaml
+''');
+  }
 }
 
 @reflectiveTest
diff --git a/pkg/analysis_server/test/lsp/diagnostic_test.dart b/pkg/analysis_server/test/lsp/diagnostic_test.dart
index fc6dffe..3b7fc18 100644
--- a/pkg/analysis_server/test/lsp/diagnostic_test.dart
+++ b/pkg/analysis_server/test/lsp/diagnostic_test.dart
@@ -88,7 +88,7 @@
 linter:
   rules:
     - invalid_lint_rule_name
-''').path;
+''');
 
     final firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
     await initialize();
@@ -102,7 +102,7 @@
   Future<void> test_analysisOptionsFile_packageInclude() async {
     newFile(analysisOptionsPath, content: '''
 include: package:pedantic/analysis_options.yaml
-''').path;
+''');
 
     // Verify there's an error for the import.
     final firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
index 9b0c895..104fc79 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
@@ -17,6 +17,7 @@
     hide Element, ElementKind;
 import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/src/utilities/charcodes.dart';
+import 'package:analyzer_plugin/src/utilities/library.dart';
 import 'package:analyzer_plugin/src/utilities/string_utilities.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
@@ -1337,11 +1338,6 @@
   /// the names used in generated code, to information about these imports.
   Map<Uri, _LibraryToImport> librariesToImport = {};
 
-  /// A mapping from libraries that need to be imported relatively in order to
-  /// make visible the names used in generated code, to information about these
-  /// imports.
-  Map<String, _LibraryToImport> librariesToRelativelyImport = {};
-
   /// Initialize a newly created builder to build a source file edit within the
   /// change being built by the given [changeBuilder]. The file being edited has
   /// the given [resolvedUnit] and [timeStamp].
@@ -1350,10 +1346,7 @@
       : super(changeBuilder, resolvedUnit.path, timeStamp);
 
   @override
-  bool get hasEdits =>
-      super.hasEdits ||
-      librariesToImport.isNotEmpty ||
-      librariesToRelativelyImport.isNotEmpty;
+  bool get hasEdits => super.hasEdits || librariesToImport.isNotEmpty;
 
   @override
   void addInsertion(
@@ -1402,9 +1395,6 @@
     for (var entry in librariesToImport.entries) {
       copy.librariesToImport[entry.key] = entry.value;
     }
-    for (var entry in librariesToRelativelyImport.entries) {
-      copy.librariesToRelativelyImport[entry.key] = entry.value;
-    }
     return copy;
   }
 
@@ -1418,9 +1408,6 @@
     if (librariesToImport.isNotEmpty) {
       _addLibraryImports(librariesToImport.values);
     }
-    if (librariesToRelativelyImport.isNotEmpty) {
-      _addLibraryImports(librariesToRelativelyImport.values);
-    }
   }
 
   @override
@@ -1480,8 +1467,12 @@
     return ImportLibraryElementResultImpl(null);
   }
 
-  String importLibraryWithRelativeUri(String uriText, [String? prefix]) {
-    return _importLibraryWithRelativeUri(uriText, prefix).uriText;
+  String importLibraryWithAbsoluteUri(Uri uri, [String? prefix]) {
+    return _importLibrary(uri, prefix: prefix, forceAbsolute: true).uriText;
+  }
+
+  String importLibraryWithRelativeUri(Uri uri, [String? prefix]) {
+    return _importLibrary(uri, prefix: prefix, forceRelative: true).uriText;
   }
 
   @override
@@ -1720,24 +1711,55 @@
   }
 
   /// Computes the best URI to import [uri] into the target library.
-  String _getLibraryUriText(Uri uri) {
-    if (uri.scheme == 'file') {
-      var pathContext = resolvedUnit.session.resourceProvider.pathContext;
-      var whatPath = pathContext.fromUri(uri);
+  ///
+  /// [uri] may be converted from an absolute URI to a relative URI depending on
+  /// user preferences/lints unless [forceAbsolute] or [forceRelative] are `true`.
+  String _getLibraryUriText(
+    Uri uri, {
+    bool forceAbsolute = false,
+    bool forceRelative = false,
+  }) {
+    var pathContext = resolvedUnit.session.resourceProvider.pathContext;
+
+    /// Returns the relative path to import [whatPath] into [resolvedUnit].
+    String getRelativePath(String whatPath) {
       var libraryPath = resolvedUnit.libraryElement.source.fullName;
       var libraryFolder = pathContext.dirname(libraryPath);
       var relativeFile = pathContext.relative(whatPath, from: libraryFolder);
       return pathContext.split(relativeFile).join('/');
     }
+
+    if (uri.isScheme('file')) {
+      var whatPath = pathContext.fromUri(uri);
+      return getRelativePath(whatPath);
+    }
+    var preferRelative = _isLintEnabled('prefer_relative_imports');
+    if (forceRelative || (preferRelative && !forceAbsolute)) {
+      if (canBeRelativeImport(uri, resolvedUnit.uri)) {
+        var whatPath = resolvedUnit.session.uriConverter.uriToPath(uri);
+        if (whatPath != null) {
+          return getRelativePath(whatPath);
+        }
+      }
+    }
     return uri.toString();
   }
 
   /// Arrange to have an import added for the library with the given [uri].
-  _LibraryToImport _importLibrary(Uri uri) {
+  ///
+  /// [uri] may be converted from an absolute URI to a relative URI depending on
+  /// user preferences/lints unless [forceAbsolute] or [forceRelative] are `true`.
+  _LibraryToImport _importLibrary(
+    Uri uri, {
+    String? prefix,
+    bool forceAbsolute = false,
+    bool forceRelative = false,
+  }) {
     var import = (libraryChangeBuilder ?? this).librariesToImport[uri];
     if (import == null) {
-      var uriText = _getLibraryUriText(uri);
-      var prefix =
+      var uriText = _getLibraryUriText(uri,
+          forceAbsolute: forceAbsolute, forceRelative: forceRelative);
+      prefix ??=
           importPrefixGenerator != null ? importPrefixGenerator!(uri) : null;
       import = _LibraryToImport(uriText, prefix);
       (libraryChangeBuilder ?? this).librariesToImport[uri] = import;
@@ -1745,23 +1767,17 @@
     return import;
   }
 
-  /// Arrange to have an import added for the library with the given relative
-  /// [uriText].
-  _LibraryToImport _importLibraryWithRelativeUri(String uriText,
-      [String? prefix]) {
-    var import = librariesToRelativelyImport[uriText];
-    if (import == null) {
-      import = _LibraryToImport(uriText, prefix);
-      librariesToRelativelyImport[uriText] = import;
-    }
-    return import;
-  }
-
   /// Return `true` if the [element] is defined in the target library.
   bool _isDefinedLocally(Element element) {
     return element.library == resolvedUnit.libraryElement;
   }
 
+  bool _isLintEnabled(String lintName) {
+    final analysisOptions =
+        resolvedUnit.session.analysisContext.analysisOptions;
+    return analysisOptions.isLintEnabled(lintName);
+  }
+
   /// Create an edit to replace the return type of the innermost function
   /// containing the given [node] with the type `Future`. The [typeProvider] is
   /// used to check the current return type, because if it is already `Future`
diff --git a/pkg/analyzer_plugin/lib/src/utilities/library.dart b/pkg/analyzer_plugin/lib/src/utilities/library.dart
new file mode 100644
index 0000000..8fa8383
--- /dev/null
+++ b/pkg/analyzer_plugin/lib/src/utilities/library.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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.
+
+/// Checks whether importing the library with URI [library2] into the
+/// library with URI [library1] could be a relative import.
+///
+/// Both URIs must be package: URIs and belong to the same package for this to
+/// be true.
+bool canBeRelativeImport(Uri library1, Uri library2) {
+  return library1.isScheme('package') &&
+      library2.isScheme('package') &&
+      library1.pathSegments.isNotEmpty &&
+      library2.pathSegments.isNotEmpty &&
+      library1.pathSegments.first == library2.pathSegments.first;
+}
diff --git a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
index 6bd6c78..4eababe 100644
--- a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
@@ -346,6 +346,9 @@
   ///
   /// Returns the text of the URI that will be used in the import directive.
   /// It can be different than the given [Uri].
+  ///
+  /// [uri] may be converted from an absolute URI to a relative URI depending on
+  /// user preferences/lints.
   String importLibrary(Uri uri);
 
   /// Ensure that the library with the given [uri] is imported.