Prototype fix to convert URI to the shorthand syntax proposed by:

https://github.com/dart-lang/language/issues/649

There are some differences between the semantics in the issue and in the
proposal: https://github.com/dart-lang/language/tree/master/working/0649%20-%20Import%20shorthand

This branch follows the issue's proposal because I think a shorthand
for relative imports is worth doing.
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 66d56b8..75f14c4 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -5,6 +5,7 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as p;
 
 import 'cli/formatter_options.dart';
@@ -12,6 +13,37 @@
 import 'exceptions.dart';
 import 'source_code.dart';
 
+/// Gets the name of the package that contains [uri] or `null` if it is not in
+/// a package.
+String? packageForUri(String? uri) {
+  if (uri == null) return null;
+
+  if (!Uri.parse(uri).isAbsolute) {
+    uri = p.url.normalize(p.url.join(Directory.current.uri.toString(), uri));
+  }
+
+  var dir = p.url.dirname(p.fromUri(uri));
+
+  // TODO: Could cache some of this work for performance.
+
+  // Find the package config containing this file in order to determine what
+  // package the file is in.
+  while (Directory(p.fromUri(dir)).existsSync()) {
+    var configFile =
+        File(p.join(p.fromUri(dir), '.dart_tool', 'package_config.json'));
+    if (configFile.existsSync()) {
+      var packageConfig = PackageConfig.parseBytes(configFile.readAsBytesSync(),
+          Uri.file(p.fromUri(p.url.join(dir, '.dart_tool'))));
+
+      return packageConfig.packageOf(Uri.parse(uri))?.name;
+    }
+
+    dir = p.url.dirname(dir);
+  }
+
+  return null;
+}
+
 /// Reads and formats input from stdin until closed.
 Future<void> formatStdin(
     FormatterOptions options, List<int>? selection, String name) async {
diff --git a/lib/src/source_visitor.dart b/lib/src/source_visitor.dart
index 4a53ae6..0cc3927 100644
--- a/lib/src/source_visitor.dart
+++ b/lib/src/source_visitor.dart
@@ -6,6 +6,7 @@
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/src/generated/source.dart';
+import 'package:dart_style/src/io.dart';
 
 import 'argument_list_visitor.dart';
 import 'call_chain_visitor.dart';
@@ -192,6 +193,8 @@
   /// from the output.
   final Set<Token> _suppressPrecedingCommentsAndNewLines = {};
 
+  late final String? _currentPackage = packageForUri(_source.uri);
+
   /// Initialize a newly created visitor to write source code representing
   /// the visited nodes to the given [writer].
   SourceVisitor(this._formatter, this._lineInfo, this._source)
@@ -931,7 +934,7 @@
 
     token(node.rightParenthesis);
     space();
-    visit(node.uri);
+    _visitUri(node.uri);
   }
 
   @override
@@ -1190,7 +1193,7 @@
     _simpleStatement(node, () {
       token(node.keyword);
       space();
-      visit(node.uri);
+      _visitUri(node.uri);
 
       _visitConfigurations(node.configurations);
 
@@ -2082,7 +2085,7 @@
     _simpleStatement(node, () {
       token(node.keyword);
       space();
-      visit(node.uri);
+      _visitUri(node.uri);
 
       _visitConfigurations(node.configurations);
 
@@ -2100,6 +2103,68 @@
     });
   }
 
+  void _visitUri(StringLiteral uriLiteral) {
+    if (_formatter.fixes.contains(StyleFix.uriShorthand)) {
+      var uri = Uri.parse(uriLiteral.stringValue!);
+
+      String? result;
+      if (uri.scheme == 'package' &&
+          uri.pathSegments.length > 1 &&
+          uri.path.endsWith('.dart')) {
+        var packageName = uri.pathSegments.first;
+        var packageShortName = packageName.split('.').last;
+
+        var path = uri.pathSegments.skip(1).join('/');
+        // Remove ".dart".
+        path = path.substring(0, path.length - 5);
+
+        if (packageShortName == path) {
+          // package:name/name.dart -> name
+          // package:dotted.name/name.dart -> name
+          result = packageName;
+        } else if (packageName == _currentPackage) {
+          // TODO: The issue uses "/" here but the proposal says ":". Using
+          // "/" to match relative paths below.
+          result = '/$path';
+        } else {
+          // package:name/path.dart -> name:path
+          result = '$packageName:$path';
+        }
+
+        // TODO: Detect if package is same as current package and use ":path".
+        // package:current_package/path.dart -> :path
+      } else if (uri.scheme == 'dart') {
+        result = 'dart:${uri.path}';
+      } else if (uri.scheme == '' && uri.path.endsWith('.dart')) {
+        // Shorthand for relative imports.
+
+        // TODO: This is described in the issue, but not the proposal.
+
+        var path = uri.path;
+        // Remove ".dart".
+        path = path.substring(0, path.length - 5);
+
+        if (!path.startsWith('.')) {
+          // TODO: If path is in root directory, should just do "/".
+          path = './$path';
+        }
+
+        result = path;
+      } else {
+        // No shorthand.
+      }
+
+      if (result != null) {
+        writePrecedingCommentsAndNewlines(uriLiteral.beginToken);
+        _writeText(result, uriLiteral.offset);
+      } else {
+        visit(uriLiteral);
+      }
+    } else {
+      visit(uriLiteral);
+    }
+  }
+
   @override
   void visitIndexExpression(IndexExpression node) {
     builder.nestExpression();
@@ -2411,7 +2476,7 @@
     _simpleStatement(node, () {
       token(node.keyword);
       space();
-      visit(node.uri);
+      _visitUri(node.uri);
     });
   }
 
@@ -2427,7 +2492,7 @@
       // Part-of may have either a name or a URI. Only one of these will be
       // non-null. We visit both since visit() ignores null.
       visit(node.libraryName);
-      visit(node.uri);
+      if (node.uri != null) _visitUri(node.uri!);
     });
   }
 
diff --git a/lib/src/style_fix.dart b/lib/src/style_fix.dart
index 032cd31..ffc99bd 100644
--- a/lib/src/style_fix.dart
+++ b/lib/src/style_fix.dart
@@ -24,6 +24,9 @@
   static const singleCascadeStatements = StyleFix._('single-cascade-statements',
       'Remove unnecessary single cascades from expression statements.');
 
+  static const uriShorthand = StyleFix._('uri-shorthand',
+      'Convert URIs in directives to shorthand syntax.');
+
   static const all = [
     docComments,
     functionTypedefs,
@@ -31,6 +34,7 @@
     optionalConst,
     optionalNew,
     singleCascadeStatements,
+    uriShorthand,
   ];
 
   final String name;
diff --git a/pubspec.lock b/pubspec.lock
index 62aa5bd..4a54c17 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -170,7 +170,7 @@
     source: hosted
     version: "2.0.1"
   package_config:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: package_config
       url: "https://pub.dartlang.org"
diff --git a/pubspec.yaml b/pubspec.yaml
index d858cac..5ccd86b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,6 +12,7 @@
 dependencies:
   analyzer: '>=2.0.0 <3.0.0'
   args: '>=1.0.0 <3.0.0'
+  package_config: ^2.0.2
   path: ^1.0.0
   pub_semver: '>=1.4.4 <3.0.0'
   source_span: ^1.4.0