[CFE] Interative spell check mode for messages

Change-Id: I61a71250159ec88cee1aafa82f3749dc3ac13c06
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/163862
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/test/comments_on_certain_arguments_tool.dart b/pkg/front_end/test/comments_on_certain_arguments_tool.dart
index 5920035..c0ff2cb 100644
--- a/pkg/front_end/test/comments_on_certain_arguments_tool.dart
+++ b/pkg/front_end/test/comments_on_certain_arguments_tool.dart
@@ -4,16 +4,7 @@
 
 import 'dart:convert' show utf8;
 import 'dart:io'
-    show
-        Directory,
-        File,
-        FileSystemEntity,
-        Platform,
-        Process,
-        ProcessResult,
-        exitCode,
-        stdin,
-        stdout;
+    show Directory, File, FileSystemEntity, exitCode, stdin, stdout;
 
 import 'package:_fe_analyzer_shared/src/messages/severity.dart' show Severity;
 import 'package:_fe_analyzer_shared/src/scanner/token.dart'
@@ -41,16 +32,9 @@
 import 'package:kernel/target/targets.dart' show TargetFlags;
 import "package:vm/target/vm.dart" show VmTarget;
 
-final Uri repoDir = _computeRepoDir();
+import "utils/io_utils.dart";
 
-Uri _computeRepoDir() {
-  ProcessResult result = Process.runSync(
-      'git', ['rev-parse', '--show-toplevel'],
-      runInShell: true,
-      workingDirectory: new File.fromUri(Platform.script).parent.path);
-  String dirPath = (result.stdout as String).trim();
-  return new Directory(dirPath).uri;
-}
+final Uri repoDir = computeRepoDirUri();
 
 Set<Uri> libUris = {};
 
diff --git a/pkg/front_end/test/explicit_creation_test.dart b/pkg/front_end/test/explicit_creation_test.dart
index 215f824..7e8246a 100644
--- a/pkg/front_end/test/explicit_creation_test.dart
+++ b/pkg/front_end/test/explicit_creation_test.dart
@@ -40,17 +40,9 @@
 import "package:vm/target/vm.dart" show VmTarget;
 
 import 'testing_utils.dart' show getGitFiles;
+import "utils/io_utils.dart";
 
-final Uri repoDir = _computeRepoDir();
-
-Uri _computeRepoDir() {
-  ProcessResult result = Process.runSync(
-      'git', ['rev-parse', '--show-toplevel'],
-      runInShell: true,
-      workingDirectory: new File.fromUri(Platform.script).parent.path);
-  String dirPath = (result.stdout as String).trim();
-  return new Directory(dirPath).uri;
-}
+final Uri repoDir = computeRepoDirUri();
 
 Set<Uri> libUris = {};
 
diff --git a/pkg/front_end/test/fasta/messages_suite.dart b/pkg/front_end/test/fasta/messages_suite.dart
index cb0f930..3a3dcd2 100644
--- a/pkg/front_end/test/fasta/messages_suite.dart
+++ b/pkg/front_end/test/fasta/messages_suite.dart
@@ -6,7 +6,7 @@
 
 import "dart:convert" show utf8;
 
-import "dart:io" show File;
+import 'dart:io' show File, Platform;
 
 import "dart:typed_data" show Uint8List;
 
@@ -97,8 +97,31 @@
   final BatchCompiler compiler;
 
   final bool fastOnly;
+  final bool interactive;
 
-  MessageTestSuite(this.fastOnly)
+  final Set<String> reportedWords = {};
+  final Set<String> reportedWordsDenylisted = {};
+
+  @override
+  Future<void> postRun() {
+    String dartPath = Platform.resolvedExecutable;
+    Uri suiteUri =
+        spell.repoDir.resolve("pkg/front_end/test/fasta/messages_suite.dart");
+    File suiteFile = new File.fromUri(suiteUri).absolute;
+    if (!suiteFile.existsSync()) {
+      throw "Specified suite path is invalid.";
+    }
+    String suitePath = suiteFile.path;
+    spell.spellSummarizeAndInteractiveMode(
+        reportedWords,
+        reportedWordsDenylisted,
+        [spell.Dictionaries.cfeMessages],
+        interactive,
+        '"$dartPath" "$suitePath" -DfastOnly=true -Dinteractive=true');
+    return null;
+  }
+
+  MessageTestSuite(this.fastOnly, this.interactive)
       : fileSystem = new MemoryFileSystem(Uri.parse("org-dartlang-fasta:///")),
         compiler = new BatchCompiler(null);
 
@@ -143,8 +166,8 @@
       Configuration configuration;
 
       Source source;
-      List<String> formatSpellingMistakes(
-          spell.SpellingResult spellResult, int offset, String message) {
+      List<String> formatSpellingMistakes(spell.SpellingResult spellResult,
+          int offset, String message, String messageForDenyListed) {
         if (source == null) {
           List<int> bytes = file.readAsBytesSync();
           List<int> lineStarts = new List<int>();
@@ -160,12 +183,20 @@
         for (int i = 0; i < spellResult.misspelledWords.length; i++) {
           Location location = source.getLocation(
               uri, offset + spellResult.misspelledWordsOffset[i]);
+          bool denylisted = spellResult.misspelledWordsDenylisted[i];
+          String messageToUse = message;
+          if (denylisted) {
+            messageToUse = messageForDenyListed;
+            reportedWordsDenylisted.add(spellResult.misspelledWords[i]);
+          } else {
+            reportedWords.add(spellResult.misspelledWords[i]);
+          }
           result.add(command_line_reporting.formatErrorMessage(
               source.getTextLine(location.line),
               location,
               spellResult.misspelledWords[i].length,
               relativize(uri),
-              "$message: '${spellResult.misspelledWords[i]}'."));
+              "$messageToUse: '${spellResult.misspelledWords[i]}'."));
         }
         return result;
       }
@@ -190,7 +221,10 @@
               spellingMessages.addAll(formatSpellingMistakes(
                   spellingResult,
                   node.span.start.offset,
-                  "Template likely has the following spelling mistake"));
+                  "Template has the following word that is "
+                      "not in our dictionary",
+                  "Template has the following word that is "
+                      "on our deny-list"));
             }
             break;
 
@@ -206,7 +240,10 @@
               spellingMessages.addAll(formatSpellingMistakes(
                   spellingResult,
                   node.span.start.offset,
-                  "Tip likely has the following spelling mistake"));
+                  "Tip has the following word that is "
+                      "not in our dictionary",
+                  "Tip has the following word that is "
+                      "on our deny-list"));
             }
             break;
 
@@ -745,7 +782,8 @@
 Future<MessageTestSuite> createContext(
     Chain suite, Map<String, String> environment) async {
   final bool fastOnly = environment["fastOnly"] == "true";
-  return new MessageTestSuite(fastOnly);
+  final bool interactive = environment["interactive"] == "true";
+  return new MessageTestSuite(fastOnly, interactive);
 }
 
 String relativize(Uri uri) {
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index 519403a..5aa5116 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -351,6 +351,7 @@
 ko
 koo
 la
+launch
 launching
 le
 legs
diff --git a/pkg/front_end/test/spell_checking_utils.dart b/pkg/front_end/test/spell_checking_utils.dart
index e29b233..948ea88 100644
--- a/pkg/front_end/test/spell_checking_utils.dart
+++ b/pkg/front_end/test/spell_checking_utils.dart
@@ -2,7 +2,11 @@
 // 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:io' show File, stdin, stdout;
+
+import "utils/io_utils.dart";
+
+final Uri repoDir = computeRepoDirUri();
 
 enum Dictionaries {
   common,
@@ -162,19 +166,18 @@
 Uri dictionaryToUri(Dictionaries dictionaryType) {
   switch (dictionaryType) {
     case Dictionaries.common:
-      return Uri.base
+      return repoDir
           .resolve("pkg/front_end/test/spell_checking_list_common.txt");
     case Dictionaries.cfeMessages:
-      return Uri.base
+      return repoDir
           .resolve("pkg/front_end/test/spell_checking_list_messages.txt");
     case Dictionaries.cfeCode:
-      return Uri.base
-          .resolve("pkg/front_end/test/spell_checking_list_code.txt");
+      return repoDir.resolve("pkg/front_end/test/spell_checking_list_code.txt");
     case Dictionaries.cfeTests:
-      return Uri.base
+      return repoDir
           .resolve("pkg/front_end/test/spell_checking_list_tests.txt");
     case Dictionaries.denylist:
-      return Uri.base
+      return repoDir
           .resolve("pkg/front_end/test/spell_checking_list_denylist.txt");
   }
   throw "Unknown Dictionary";
@@ -342,3 +345,121 @@
   }
   return result;
 }
+
+void spellSummarizeAndInteractiveMode(
+    Set<String> reportedWords,
+    Set<String> reportedWordsDenylisted,
+    List<Dictionaries> dictionaries,
+    bool interactive,
+    String interactiveLaunchExample) {
+  if (reportedWordsDenylisted.isNotEmpty) {
+    print("\n\n\n");
+    print("================");
+    print("The following words was reported as used and denylisted:");
+    print("----------------");
+    for (String s in reportedWordsDenylisted) {
+      print("$s");
+    }
+    print("================");
+  }
+  if (reportedWords.isNotEmpty) {
+    print("\n\n\n");
+    print("================");
+    print("The following word(s) were reported as unknown:");
+    print("----------------");
+
+    Dictionaries dictionaryToUse;
+    if (dictionaries.contains(Dictionaries.cfeTests)) {
+      dictionaryToUse = Dictionaries.cfeTests;
+    } else if (dictionaries.contains(Dictionaries.cfeMessages)) {
+      dictionaryToUse = Dictionaries.cfeMessages;
+    } else if (dictionaries.contains(Dictionaries.cfeCode)) {
+      dictionaryToUse = Dictionaries.cfeCode;
+    } else {
+      for (Dictionaries dictionary in dictionaries) {
+        if (dictionaryToUse == null ||
+            dictionary.index < dictionaryToUse.index) {
+          dictionaryToUse = dictionary;
+        }
+      }
+    }
+
+    if (interactive && dictionaryToUse != null) {
+      List<String> addedWords = new List<String>();
+      for (String s in reportedWords) {
+        print("- $s");
+        String answer;
+        bool add;
+        while (true) {
+          stdout.write("Do you want to add the word to the dictionary "
+              "$dictionaryToUse (y/n)? ");
+          answer = stdin.readLineSync().trim().toLowerCase();
+          switch (answer) {
+            case "y":
+            case "yes":
+            case "true":
+              add = true;
+              break;
+            case "n":
+            case "no":
+            case "false":
+              add = false;
+              break;
+            default:
+              add = null;
+              print("'$answer' is not a valid answer. Please try again.");
+              break;
+          }
+          if (add != null) break;
+        }
+        if (add) {
+          addedWords.add(s);
+        }
+      }
+      if (addedWords.isNotEmpty) {
+        File dictionaryFile =
+            new File.fromUri(dictionaryToUri(dictionaryToUse));
+        List<String> lines = dictionaryFile.readAsLinesSync();
+        List<String> header = new List<String>();
+        List<String> sortThis = new List<String>();
+        for (String line in lines) {
+          if (line.startsWith("#")) {
+            header.add(line);
+          } else if (line.trim().isEmpty && sortThis.isEmpty) {
+            header.add(line);
+          } else if (line.trim().isNotEmpty) {
+            sortThis.add(line);
+          }
+        }
+        sortThis.addAll(addedWords);
+        sortThis.sort();
+        lines = new List<String>();
+        lines.addAll(header);
+        if (header.isEmpty || header.last.isNotEmpty) {
+          lines.add("");
+        }
+        lines.addAll(sortThis);
+        lines.add("");
+        dictionaryFile.writeAsStringSync(lines.join("\n"));
+      }
+    } else {
+      for (String s in reportedWords) {
+        print("$s");
+      }
+      if (dictionaries.isNotEmpty) {
+        print("----------------");
+        print("If the word(s) are correctly spelled please add it to one of "
+            "these files:");
+        for (Dictionaries dictionary in dictionaries) {
+          print(" - ${dictionaryToUri(dictionary)}");
+        }
+
+        print("");
+        print("To add words easily, try to run this script in interactive "
+            "mode via the command");
+        print(interactiveLaunchExample);
+      }
+    }
+    print("================");
+  }
+}
diff --git a/pkg/front_end/test/spelling_test_base.dart b/pkg/front_end/test/spelling_test_base.dart
index 9879c8c..9bafa38 100644
--- a/pkg/front_end/test/spelling_test_base.dart
+++ b/pkg/front_end/test/spelling_test_base.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async' show Future;
 
-import 'dart:io' show File, Platform, stdin, stdout;
+import 'dart:io' show File, Platform;
 
 import 'dart:typed_data' show Uint8List;
 
@@ -51,6 +51,8 @@
 
   bool get onlyDenylisted;
 
+  String get repoRelativeSuitePath;
+
   Set<String> reportedWords = {};
   Set<String> reportedWordsDenylisted = {};
 
@@ -61,111 +63,19 @@
 
   @override
   Future<void> postRun() {
-    if (reportedWordsDenylisted.isNotEmpty) {
-      print("\n\n\n");
-      print("================");
-      print("The following words was reported as used and denylisted:");
-      print("----------------");
-      for (String s in reportedWordsDenylisted) {
-        print("$s");
-      }
-      print("================");
+    String dartPath = Platform.resolvedExecutable;
+    Uri suiteUri = spell.repoDir.resolve(repoRelativeSuitePath);
+    File suiteFile = new File.fromUri(suiteUri).absolute;
+    if (!suiteFile.existsSync()) {
+      throw "Specified suite path is invalid.";
     }
-    if (reportedWords.isNotEmpty) {
-      print("\n\n\n");
-      print("================");
-      print("The following word(s) were reported as unknown:");
-      print("----------------");
-
-      spell.Dictionaries dictionaryToUse;
-      if (dictionaries.contains(spell.Dictionaries.cfeTests)) {
-        dictionaryToUse = spell.Dictionaries.cfeTests;
-      } else if (dictionaries.contains(spell.Dictionaries.cfeMessages)) {
-        dictionaryToUse = spell.Dictionaries.cfeMessages;
-      } else if (dictionaries.contains(spell.Dictionaries.cfeCode)) {
-        dictionaryToUse = spell.Dictionaries.cfeCode;
-      } else {
-        for (spell.Dictionaries dictionary in dictionaries) {
-          if (dictionaryToUse == null ||
-              dictionary.index < dictionaryToUse.index) {
-            dictionaryToUse = dictionary;
-          }
-        }
-      }
-
-      if (interactive && dictionaryToUse != null) {
-        List<String> addedWords = new List<String>();
-        for (String s in reportedWords) {
-          print("- $s");
-          stdout.write("Do you want to add the word to the dictionary "
-              "$dictionaryToUse (y/n)? ");
-          String answer = stdin.readLineSync().trim().toLowerCase();
-          bool add;
-          switch (answer) {
-            case "y":
-            case "yes":
-            case "true":
-              add = true;
-              break;
-            case "n":
-            case "no":
-            case "false":
-              add = false;
-              break;
-            default:
-              throw "Didn't understand '$answer'";
-          }
-          if (add) {
-            addedWords.add(s);
-          }
-        }
-        if (addedWords.isNotEmpty) {
-          File dictionaryFile =
-              new File.fromUri(spell.dictionaryToUri(dictionaryToUse));
-          List<String> lines = dictionaryFile.readAsLinesSync();
-          List<String> header = new List<String>();
-          List<String> sortThis = new List<String>();
-          for (String line in lines) {
-            if (line.startsWith("#")) {
-              header.add(line);
-            } else if (line.trim().isEmpty && sortThis.isEmpty) {
-              header.add(line);
-            } else if (line.trim().isNotEmpty) {
-              sortThis.add(line);
-            }
-          }
-          sortThis.addAll(addedWords);
-          sortThis.sort();
-          lines = new List<String>();
-          lines.addAll(header);
-          if (header.isEmpty || header.last.isNotEmpty) {
-            lines.add("");
-          }
-          lines.addAll(sortThis);
-          lines.add("");
-          dictionaryFile.writeAsStringSync(lines.join("\n"));
-        }
-      } else {
-        for (String s in reportedWords) {
-          print("$s");
-        }
-        if (dictionaries.isNotEmpty) {
-          print("----------------");
-          print("If the word(s) are correctly spelled please add it to one of "
-              "these files:");
-          for (spell.Dictionaries dictionary in dictionaries) {
-            print(" - ${spell.dictionaryToUri(dictionary)}");
-          }
-
-          print("");
-          print("To add words easily, try to run this script in interactive "
-              "mode via the command");
-          print("dart ${Platform.script.toFilePath()} "
-              "-DonlyInGit=$onlyInGit -Dinteractive=true");
-        }
-      }
-      print("================");
-    }
+    String suitePath = suiteFile.path;
+    spell.spellSummarizeAndInteractiveMode(
+        reportedWords,
+        reportedWordsDenylisted,
+        dictionaries,
+        interactive,
+        '"$dartPath" "$suitePath" -DonlyInGit=$onlyInGit -Dinteractive=true');
     return null;
   }
 }
diff --git a/pkg/front_end/test/spelling_test_external_targets.dart b/pkg/front_end/test/spelling_test_external_targets.dart
index f339a46..0330b22 100644
--- a/pkg/front_end/test/spelling_test_external_targets.dart
+++ b/pkg/front_end/test/spelling_test_external_targets.dart
@@ -39,6 +39,10 @@
   @override
   bool get onlyDenylisted => true;
 
+  @override
+  String get repoRelativeSuitePath =>
+      "pkg/front_end/test/spelling_test_external_targets.dart";
+
   Stream<TestDescription> list(Chain suite) async* {
     for (String subdir in const ["pkg/", "sdk/"]) {
       Directory testRoot = new Directory.fromUri(suite.uri.resolve(subdir));
diff --git a/pkg/front_end/test/spelling_test_not_src_suite.dart b/pkg/front_end/test/spelling_test_not_src_suite.dart
index 28d5262..3259c88 100644
--- a/pkg/front_end/test/spelling_test_not_src_suite.dart
+++ b/pkg/front_end/test/spelling_test_not_src_suite.dart
@@ -38,4 +38,8 @@
 
   @override
   bool get onlyDenylisted => false;
+
+  @override
+  String get repoRelativeSuitePath =>
+      "pkg/front_end/test/spelling_test_not_src_suite.dart";
 }
diff --git a/pkg/front_end/test/spelling_test_src_suite.dart b/pkg/front_end/test/spelling_test_src_suite.dart
index 4fe9568..afea1e0 100644
--- a/pkg/front_end/test/spelling_test_src_suite.dart
+++ b/pkg/front_end/test/spelling_test_src_suite.dart
@@ -37,4 +37,8 @@
 
   @override
   bool get onlyDenylisted => false;
+
+  @override
+  String get repoRelativeSuitePath =>
+      "pkg/front_end/test/spelling_test_src_suite.dart";
 }