8a883fa54d
Leaves some in parser test: pkg/front_end/parser_testcases/error_recovery/keyword_named_formal_parameter_prime.dart TEST=Refactoring, covered by existing tests. Change-Id: I7a83ef95df3cbd283878b3685b5c747bd89a1b16 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256125 Reviewed-by: Johnni Winther <johnniwinther@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com>
465 lines
15 KiB
Dart
465 lines
15 KiB
Dart
// Copyright (c) 2019, 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.
|
|
|
|
import 'dart:io' show File, stdin, stdout;
|
|
|
|
import "utils/io_utils.dart";
|
|
|
|
final Uri repoDir = computeRepoDirUri();
|
|
|
|
enum Dictionaries {
|
|
common,
|
|
cfeMessages,
|
|
cfeCode,
|
|
cfeTests,
|
|
// The denylist is special and is always loaded!
|
|
denylist,
|
|
}
|
|
|
|
Map<Dictionaries, Set<String>>? loadedDictionaries;
|
|
|
|
SpellingResult spellcheckString(String s,
|
|
{List<Dictionaries>? dictionaries, bool splitAsCode = false}) {
|
|
dictionaries ??= const [Dictionaries.common];
|
|
ensureDictionariesLoaded(dictionaries);
|
|
|
|
List<String>? wrongWords;
|
|
List<List<String>?>? wrongWordsAlternatives;
|
|
List<int>? wrongWordsOffset;
|
|
List<bool>? wrongWordDenylisted;
|
|
List<int> wordOffsets = <int>[];
|
|
List<String> words =
|
|
splitStringIntoWords(s, wordOffsets, splitAsCode: splitAsCode);
|
|
List<Set<String>> dictionariesUnpacked = [];
|
|
for (int j = 0; j < dictionaries.length; j++) {
|
|
Dictionaries dictionaryType = dictionaries[j];
|
|
if (dictionaryType == Dictionaries.denylist) continue;
|
|
Set<String> dictionary = loadedDictionaries![dictionaryType]!;
|
|
dictionariesUnpacked.add(dictionary);
|
|
}
|
|
for (int i = 0; i < words.length; i++) {
|
|
String word = words[i].toLowerCase();
|
|
int offset = wordOffsets[i];
|
|
bool found = false;
|
|
|
|
for (int j = 0; j < dictionariesUnpacked.length; j++) {
|
|
Set<String> dictionary = dictionariesUnpacked[j];
|
|
if (dictionary.contains(word)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
wrongWords ??= [];
|
|
wrongWords.add(word);
|
|
wrongWordsAlternatives ??= [];
|
|
wrongWordsAlternatives.add(findAlternatives(word, dictionariesUnpacked));
|
|
wrongWordsOffset ??= [];
|
|
wrongWordsOffset.add(offset);
|
|
wrongWordDenylisted ??= [];
|
|
wrongWordDenylisted
|
|
.add(loadedDictionaries![Dictionaries.denylist]!.contains(word));
|
|
}
|
|
}
|
|
|
|
return new SpellingResult(wrongWords, wrongWordsOffset, wrongWordDenylisted,
|
|
wrongWordsAlternatives);
|
|
}
|
|
|
|
List<String>? findAlternatives(String word, List<Set<String>> dictionaries) {
|
|
List<String>? result;
|
|
|
|
bool check(String w) {
|
|
for (int j = 0; j < dictionaries.length; j++) {
|
|
Set<String> dictionary = dictionaries[j];
|
|
if (dictionary.contains(w)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ok(String w) {
|
|
(result ??= <String>[]).add(w);
|
|
}
|
|
|
|
// 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 deletedLetter = before + after;
|
|
if (check(deletedLetter)) ok(deletedLetter);
|
|
}
|
|
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;
|
|
if (check(replacedLetter)) ok(replacedLetter);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
class SpellingResult {
|
|
final List<String>? misspelledWords;
|
|
final List<int>? misspelledWordsOffset;
|
|
final List<bool>? misspelledWordsDenylisted;
|
|
final List<List<String>?>? misspelledWordsAlternatives;
|
|
|
|
SpellingResult(this.misspelledWords, this.misspelledWordsOffset,
|
|
this.misspelledWordsDenylisted, this.misspelledWordsAlternatives);
|
|
}
|
|
|
|
void ensureDictionariesLoaded(List<Dictionaries> dictionaries) {
|
|
void addWords(Uri uri, Set<String> dictionary) {
|
|
for (String word in File.fromUri(uri)
|
|
.readAsStringSync()
|
|
.split("\n")
|
|
.map((s) => s.toLowerCase())) {
|
|
if (word.startsWith("#")) continue;
|
|
int indexOfHash = word.indexOf(" #");
|
|
if (indexOfHash >= 0) {
|
|
// Strip out comment.
|
|
word = word.substring(0, indexOfHash).trim();
|
|
}
|
|
if (word == "") continue;
|
|
if (word.contains(" ")) throw "'$word' contains spaces";
|
|
dictionary.add(word);
|
|
}
|
|
}
|
|
|
|
loadedDictionaries ??= new Map<Dictionaries, Set<String>>();
|
|
// Ensure the denylist is loaded.
|
|
Set<String>? denylistDictionary = loadedDictionaries![Dictionaries.denylist];
|
|
if (denylistDictionary == null) {
|
|
denylistDictionary = new Set<String>();
|
|
loadedDictionaries![Dictionaries.denylist] = denylistDictionary;
|
|
addWords(dictionaryToUri(Dictionaries.denylist), denylistDictionary);
|
|
}
|
|
|
|
for (int j = 0; j < dictionaries.length; j++) {
|
|
Dictionaries dictionaryType = dictionaries[j];
|
|
Set<String>? dictionary = loadedDictionaries![dictionaryType];
|
|
if (dictionary == null) {
|
|
dictionary = new Set<String>();
|
|
loadedDictionaries![dictionaryType] = dictionary;
|
|
addWords(dictionaryToUri(dictionaryType), dictionary);
|
|
// Check that no good words occur in the denylist.
|
|
for (String s in dictionary) {
|
|
if (denylistDictionary.contains(s)) {
|
|
throw "Word '$s' in dictionary $dictionaryType "
|
|
"is also in the denylist.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Uri dictionaryToUri(Dictionaries dictionaryType) {
|
|
switch (dictionaryType) {
|
|
case Dictionaries.common:
|
|
return repoDir
|
|
.resolve("pkg/front_end/test/spell_checking_list_common.txt");
|
|
case Dictionaries.cfeMessages:
|
|
return repoDir
|
|
.resolve("pkg/front_end/test/spell_checking_list_messages.txt");
|
|
case Dictionaries.cfeCode:
|
|
return repoDir.resolve("pkg/front_end/test/spell_checking_list_code.txt");
|
|
case Dictionaries.cfeTests:
|
|
return repoDir
|
|
.resolve("pkg/front_end/test/spell_checking_list_tests.txt");
|
|
case Dictionaries.denylist:
|
|
return repoDir
|
|
.resolve("pkg/front_end/test/spell_checking_list_denylist.txt");
|
|
}
|
|
}
|
|
|
|
List<String> splitStringIntoWords(String s, List<int> splitOffsets,
|
|
{bool splitAsCode = false}) {
|
|
List<String> result = <String>[];
|
|
// Match whitespace and the characters "-", "=", "|", "/", ",".
|
|
String regExpStringInner = r"\s-=\|\/,";
|
|
if (splitAsCode) {
|
|
// If splitting as code also split by "_", ":", ".", "(", ")", "<", ">",
|
|
// "[", "]", "{", "}", "@", "&", "#", "?", "%", "`", '"', and numbers.
|
|
// (As well as doing stuff to camel casing further below).
|
|
regExpStringInner =
|
|
"${regExpStringInner}_:\\.\\(\\)<>\\[\\]\{\}@&#\\?%`\"0123456789";
|
|
}
|
|
// Match one or more of the characters specified above.
|
|
String regExp = "[$regExpStringInner]+";
|
|
if (splitAsCode) {
|
|
// If splitting as code we also want to remove the two characters "\n".
|
|
regExp = "([$regExpStringInner]|(\\\\n))+";
|
|
}
|
|
|
|
Iterator<RegExpMatch> matchesIterator =
|
|
new RegExp(regExp).allMatches(s).iterator;
|
|
int latestMatch = 0;
|
|
List<String> split = <String>[];
|
|
List<int> splitOffset = <int>[];
|
|
while (matchesIterator.moveNext()) {
|
|
RegExpMatch match = matchesIterator.current;
|
|
if (match.start > latestMatch) {
|
|
split.add(s.substring(latestMatch, match.start));
|
|
splitOffset.add(latestMatch);
|
|
}
|
|
latestMatch = match.end;
|
|
}
|
|
if (s.length > latestMatch) {
|
|
split.add(s.substring(latestMatch, s.length));
|
|
splitOffset.add(latestMatch);
|
|
}
|
|
|
|
for (int i = 0; i < split.length; i++) {
|
|
String word = split[i];
|
|
int offset = splitOffset[i];
|
|
if (word.isEmpty) continue;
|
|
int start = 0;
|
|
int end = word.length;
|
|
bool changedStart = false;
|
|
while (start < end) {
|
|
int unit = word.codeUnitAt(start);
|
|
if (unit >= 65 && unit <= 90) {
|
|
// A-Z => Good.
|
|
break;
|
|
} else if (unit >= 97 && unit <= 122) {
|
|
// a-z => Good.
|
|
break;
|
|
} else {
|
|
changedStart = true;
|
|
start++;
|
|
}
|
|
}
|
|
bool changedEnd = false;
|
|
while (end > start) {
|
|
int unit = word.codeUnitAt(end - 1);
|
|
if (unit >= 65 && unit <= 90) {
|
|
// A-Z => Good.
|
|
break;
|
|
} else if (unit >= 97 && unit <= 122) {
|
|
// a-z => Good.
|
|
break;
|
|
} else {
|
|
changedEnd = true;
|
|
end--;
|
|
}
|
|
}
|
|
if (changedEnd && word.codeUnitAt(end) == 41) {
|
|
// Special case trimmed ')' if there's a '(' inside the string.
|
|
for (int i = start; i < end; i++) {
|
|
if (word.codeUnitAt(i) == 40) {
|
|
end++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (start == end) continue;
|
|
|
|
if (splitAsCode) {
|
|
bool prevCapitalized = false;
|
|
for (int i = start; i < end; i++) {
|
|
bool thisCapitalized = false;
|
|
int unit = word.codeUnitAt(i);
|
|
if (unit >= 65 && unit <= 90) {
|
|
thisCapitalized = true;
|
|
} else if (unit >= 48 && unit <= 57) {
|
|
// Number inside --- allow that.
|
|
continue;
|
|
}
|
|
if (prevCapitalized && thisCapitalized) {
|
|
// Sort-of-weird thing, something like "thisIsTheCNN". Carry on.
|
|
|
|
// Except if the previous was an 'A' and that both the previous
|
|
// (before that) and the next (if any) is not capitalized, i.e.
|
|
// we special-case the case of 'A' as in 'AWord' being 'a word'.
|
|
int prevUnit = word.codeUnitAt(i - 1);
|
|
if (prevUnit == 65) {
|
|
bool doSpecialCase = true;
|
|
if (i + 1 < end) {
|
|
int nextUnit = word.codeUnitAt(i + 1);
|
|
if (nextUnit >= 65 && nextUnit <= 90) {
|
|
// Next is capitalized too.
|
|
doSpecialCase = false;
|
|
}
|
|
}
|
|
if (i - 2 >= start) {
|
|
int prevPrevUnit = word.codeUnitAt(i - 2);
|
|
if (prevPrevUnit >= 65 && prevPrevUnit <= 90) {
|
|
// Prev-prev was capitalized too.
|
|
doSpecialCase = false;
|
|
}
|
|
}
|
|
if (doSpecialCase) {
|
|
result.add(word.substring(start, i));
|
|
splitOffsets.add(offset + start);
|
|
start = i;
|
|
}
|
|
}
|
|
|
|
// And the case where the next one is not capitalized --- we must
|
|
// assume that "TheCNNAlso" should be "The", "CNN", "Also".
|
|
if (start < i && i + 1 < end) {
|
|
int nextUnit = word.codeUnitAt(i + 1);
|
|
if (nextUnit >= 97 && nextUnit <= 122) {
|
|
// Next is not capitalized.
|
|
result.add(word.substring(start, i));
|
|
splitOffsets.add(offset + start);
|
|
start = i;
|
|
}
|
|
}
|
|
} else if (!prevCapitalized && thisCapitalized) {
|
|
// Starting a new camel case word.
|
|
if (i > start) {
|
|
result.add(word.substring(start, i));
|
|
splitOffsets.add(offset + start);
|
|
start = i;
|
|
}
|
|
} else if (prevCapitalized && !thisCapitalized) {
|
|
// This should have been handled above.
|
|
} else if (!prevCapitalized && !thisCapitalized) {
|
|
// Continued word.
|
|
}
|
|
if (i + 1 == end) {
|
|
// End of string.
|
|
if (i >= start) {
|
|
result.add(word.substring(start, end));
|
|
splitOffsets.add(offset + start);
|
|
}
|
|
}
|
|
prevCapitalized = thisCapitalized;
|
|
}
|
|
} else {
|
|
result.add(
|
|
(changedStart || changedEnd) ? word.substring(start, end) : word);
|
|
splitOffsets.add(offset + start);
|
|
}
|
|
}
|
|
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 = <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 = <String>[];
|
|
List<String> sortThis = <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 = <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("================");
|
|
}
|
|
}
|