[CFE] Spell checker improvements

- Spell checker in interactive mode shows the 'close words' (if any)
  when asking if wanting to add a word to the directory. Hopefully this
  will make it less likely adding wrong words to the dictionary.
- When finding 'close' words it now also tries to insert a letter at the
  end which it erroneously didn't before. The close words method is also
  tested better.

Change-Id: Ic2628fc261ecb4d9c9322a0ab8c462b174707051
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403863
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/test/messages_suite.dart b/pkg/front_end/test/messages_suite.dart
index c4d5a75..b476ccd 100644
--- a/pkg/front_end/test/messages_suite.dart
+++ b/pkg/front_end/test/messages_suite.dart
@@ -89,7 +89,7 @@
   final bool fastOnly;
   final bool interactive;
 
-  final Set<String> reportedWords = {};
+  final Map<String, List<String>?> reportedWordsAndAlternatives = {};
   final Set<String> reportedWordsDenylisted = {};
 
   @override
@@ -103,7 +103,7 @@
     }
     String suitePath = suiteFile.path;
     spell.spellSummarizeAndInteractiveMode(
-        reportedWords,
+        reportedWordsAndAlternatives,
         reportedWordsDenylisted,
         [spell.Dictionaries.cfeMessages],
         interactive,
@@ -182,7 +182,8 @@
             messageToUse = messageForDenyListed;
             reportedWordsDenylisted.add(spellResult.misspelledWords![i]);
           } else {
-            reportedWords.add(spellResult.misspelledWords![i]);
+            reportedWordsAndAlternatives[spellResult.misspelledWords![i]] =
+                spellResult.misspelledWordsAlternatives![i];
           }
           result.add(command_line_reporting.formatErrorMessage(
               source!.getTextLine(location.line),
diff --git a/pkg/front_end/test/spell_checking_utils.dart b/pkg/front_end/test/spell_checking_utils.dart
index 2f3c928..508d229 100644
--- a/pkg/front_end/test/spell_checking_utils.dart
+++ b/pkg/front_end/test/spell_checking_utils.dart
@@ -82,28 +82,36 @@
     (result ??= <String>[]).add(w);
   }
 
+  void checkInsert(String before, String afterIncluding) {
+    for (int j = 0; j < 25; j++) {
+      String c = new String.fromCharCode(97 + j);
+      String insertedLetter = "${before}${c}${afterIncluding}";
+      if (check(insertedLetter)) ok(insertedLetter);
+    }
+  }
+
   // Delete a letter, insert a letter or change a letter and lookup.
   for (int i = 0; i < word.length; i++) {
     String before = word.substring(0, i);
-    String after = word.substring(i + 1);
-    String afterIncluding = word.substring(i);
+    String afterExcluding = word.substring(i + 1);
 
     {
-      String deletedLetter = before + after;
+      String deletedLetter = "${before}${afterExcluding}";
       if (check(deletedLetter)) ok(deletedLetter);
     }
+
+    checkInsert(before, word.substring(i));
+
     for (int j = 0; j < 25; j++) {
       String c = new String.fromCharCode(97 + j);
-      String insertedLetter = before + c + afterIncluding;
-      if (check(insertedLetter)) ok(insertedLetter);
-    }
-    for (int j = 0; j < 25; j++) {
-      String c = new String.fromCharCode(97 + j);
-      String replacedLetter = before + c + after;
+      String replacedLetter = "${before}${c}${afterExcluding}";
       if (check(replacedLetter)) ok(replacedLetter);
     }
   }
 
+  // Check insert at end.
+  checkInsert(word, "");
+
   return result;
 }
 
@@ -347,7 +355,7 @@
 }
 
 void spellSummarizeAndInteractiveMode(
-    Set<String> reportedWords,
+    Map<String, List<String>?> reportedWordsAndAlternatives,
     Set<String> reportedWordsDenylisted,
     List<Dictionaries> dictionaries,
     bool interactive,
@@ -365,8 +373,8 @@
     }
     print("================");
   }
-  if (reportedWords.isNotEmpty) {
-    bool isSingular = reportedWords.length == 1;
+  if (reportedWordsAndAlternatives.isNotEmpty) {
+    bool isSingular = reportedWordsAndAlternatives.length == 1;
     String suffix = isSingular ? "" : "s";
     String were = isSingular ? "was" : "were";
     String are = isSingular ? "is" : "are";
@@ -394,8 +402,15 @@
 
     if (interactive && dictionaryToUse != null) {
       List<String> addedWords = <String>[];
-      for (String s in reportedWords) {
-        print("- $s");
+      for (MapEntry<String, List<String>?> wordAndAlternative
+          in reportedWordsAndAlternatives.entries) {
+        String s = wordAndAlternative.key;
+        List<String>? alternative = wordAndAlternative.value;
+        if (alternative != null) {
+          print(" - $s (notice close word(s)): ${alternative.join(", ")})");
+        } else {
+          print(" - $s");
+        }
         String answer;
         bool? add;
         while (true) {
@@ -451,7 +466,7 @@
         dictionaryFile.writeAsStringSync(lines.join("\n"));
       }
     } else {
-      for (String s in reportedWords) {
+      for (String s in reportedWordsAndAlternatives.keys) {
         print("$s");
       }
       if (dictionaries.isNotEmpty) {
diff --git a/pkg/front_end/test/spell_checking_utils_test.dart b/pkg/front_end/test/spell_checking_utils_test.dart
index c4aa4dc..797caf4 100644
--- a/pkg/front_end/test/spell_checking_utils_test.dart
+++ b/pkg/front_end/test/spell_checking_utils_test.dart
@@ -90,6 +90,33 @@
       "explicitley", ["explicitly"], {"foo", "explicitly", "bar"});
   expectAlternative("explicitlqqqqy", null, {"foo", "explicitly", "bar"});
 
+  // Insert first letter.
+  expectAlternative("ar", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Insert middle letter.
+  expectAlternative("br", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Insert last letter.
+  expectAlternative("ba", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Delete first letter.
+  expectAlternative("xbar", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Delete middle letter.
+  expectAlternative("bxar", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Delete last letter.
+  expectAlternative("barx", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Replace first letter.
+  expectAlternative("car", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Replace middle letter.
+  expectAlternative("bcr", ["bar"], {"foo", "explicitly", "bar"});
+
+  // Replace last letter.
+  expectAlternative("bac", ["bar"], {"foo", "explicitly", "bar"});
+
   print("OK");
 }
 
diff --git a/pkg/front_end/test/spelling_test_base.dart b/pkg/front_end/test/spelling_test_base.dart
index b60c5b6..ffef67f 100644
--- a/pkg/front_end/test/spelling_test_base.dart
+++ b/pkg/front_end/test/spelling_test_base.dart
@@ -36,7 +36,7 @@
 
   String get repoRelativeSuitePath;
 
-  Set<String> reportedWords = {};
+  Map<String, List<String>?> reportedWordsAndAlternatives = {};
   Set<String> reportedWordsDenylisted = {};
 
   @override
@@ -54,7 +54,7 @@
     }
     String suitePath = suiteFile.path;
     spell.spellSummarizeAndInteractiveMode(
-        reportedWords,
+        reportedWordsAndAlternatives,
         reportedWordsDenylisted,
         dictionaries,
         interactive,
@@ -92,7 +92,7 @@
         context.reportedWordsDenylisted.add(word);
       } else {
         message = "The word '$word' is not in our dictionary.";
-        context.reportedWords.add(word);
+        context.reportedWordsAndAlternatives[word] = alternatives;
       }
       if (alternatives != null && alternatives.isNotEmpty) {
         message += "\n\nThe following word(s) was 'close' "