Skip to content

Commit

Permalink
Merge pull request #6569 from dotnet/spellChecker
Browse files Browse the repository at this point in the history
Improve performance of spell checking analyzer
  • Loading branch information
CyrusNajmabadi authored Apr 5, 2023
2 parents 7e98f62 + bac0aee commit 4208547
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 63 deletions.
44 changes: 21 additions & 23 deletions src/Text.Analyzers/Core/CodeAnalysisDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,21 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace Text.Analyzers
{
/// <summary>
/// Source for "recognized" misspellings and "unrecognized" spellings obtained by parsing either
/// XML or DIC code analysis dictionaries.
/// Source for "recognized" misspellings and "unrecognized" spellings obtained by parsing either XML or DIC code
/// analysis dictionaries.
/// </summary>
/// <Remarks>
/// <seealso href="https://learn.microsoft.com/visualstudio/code-quality/how-to-customize-the-code-analysis-dictionary"/>
/// </Remarks>
internal sealed class CodeAnalysisDictionary
{
/// <summary>
/// Initialize a new instance of <see cref="CodeAnalysisDictionary"/>.
/// </summary>
/// <param name="recognizedWords">Misspelled words that the spell checker will now ignore.</param>
/// <param name="unrecognizedWords">Correctly spelled words that the spell checker will now report.</param>
private CodeAnalysisDictionary(IEnumerable<string> recognizedWords, IEnumerable<string> unrecognizedWords)
{
RecognizedWords = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, recognizedWords.ToArray());
UnrecognizedWords = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, unrecognizedWords.ToArray());
}

/// <summary>
/// A list of misspelled words that the spell checker will now ignore.
/// </summary>
Expand All @@ -39,7 +27,7 @@ private CodeAnalysisDictionary(IEnumerable<string> recognizedWords, IEnumerable<
/// </Recognized>
/// </code>
/// </example>
public ImmutableHashSet<string> RecognizedWords { get; }
private readonly HashSet<string> _recognizedWords;

/// <summary>
/// A list of correctly spelled words that the spell checker will now report.
Expand All @@ -51,7 +39,18 @@ private CodeAnalysisDictionary(IEnumerable<string> recognizedWords, IEnumerable<
/// </Unrecognized>
/// </code>
/// </example>
public ImmutableHashSet<string> UnrecognizedWords { get; }
private readonly HashSet<string> _unrecognizedWords;

/// <summary>
/// Initialize a new instance of <see cref="CodeAnalysisDictionary"/>.
/// </summary>
/// <param name="recognizedWords">Misspelled words that the spell checker will now ignore.</param>
/// <param name="unrecognizedWords">Correctly spelled words that the spell checker will now report.</param>
private CodeAnalysisDictionary(IEnumerable<string> recognizedWords, IEnumerable<string> unrecognizedWords)
{
_recognizedWords = new HashSet<string>(recognizedWords, StringComparer.OrdinalIgnoreCase);
_unrecognizedWords = new HashSet<string>(unrecognizedWords, StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Creates a new instance of this class with recognized and unrecognized words (if specified) loaded
Expand Down Expand Up @@ -97,14 +96,13 @@ public static CodeAnalysisDictionary CreateFromDic(StreamReader streamReader)
return new CodeAnalysisDictionary(recognizedWords, Enumerable.Empty<string>());
}

public static CodeAnalysisDictionary CreateFromDictionaries(IEnumerable<CodeAnalysisDictionary> dictionaries)
{
var recognizedWords = dictionaries.Select(x => x.RecognizedWords).Aggregate((x, y) => x.Union(y));
var unrecognizedWords = dictionaries.Select(x => x.UnrecognizedWords).Aggregate((x, y) => x.Union(y));
return new CodeAnalysisDictionary(recognizedWords, unrecognizedWords);
}

private static IEnumerable<string> GetSectionWords(XDocument document, string section, string property)
=> document.Descendants(section).SelectMany(section => section.Elements(property)).Select(element => element.Value.Trim());

public bool ContainsUnrecognizedWord(string word)
=> _unrecognizedWords.Contains(word);

public bool ContainsRecognizedWord(string word)
=> _recognizedWords.Contains(word);
}
}
83 changes: 43 additions & 40 deletions src/Text.Analyzers/Core/IdentifiersShouldBeSpelledCorrectly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public sealed class IdentifiersShouldBeSpelledCorrectlyAnalyzer : DiagnosticAnal
private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(IdentifiersShouldBeSpelledCorrectlyTitle));
private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(IdentifiersShouldBeSpelledCorrectlyDescription));

private static readonly SourceTextValueProvider<CodeAnalysisDictionary> s_xmlDictionaryProvider = new(ParseXmlDictionary);
private static readonly SourceTextValueProvider<CodeAnalysisDictionary> s_dicDictionaryProvider = new(ParseDicDictionary);
private static readonly SourceTextValueProvider<(CodeAnalysisDictionary dictionary, Exception? exception)> s_xmlDictionaryProvider = new(static text => ParseDictionary(text, isXml: true));
private static readonly SourceTextValueProvider<(CodeAnalysisDictionary dictionary, Exception? exception)> s_dicDictionaryProvider = new(static text => ParseDictionary(text, isXml: false));
private static readonly CodeAnalysisDictionary s_mainDictionary = GetMainDictionary();

internal static readonly DiagnosticDescriptor FileParseRule = DiagnosticDescriptorHelper.Create(
Expand Down Expand Up @@ -238,14 +238,15 @@ public override void Initialize(AnalysisContext context)
context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext compilationStartContext)
private void OnCompilationStart(CompilationStartAnalysisContext context)
{
var dictionaries = ReadDictionaries();
var projectDictionary = CodeAnalysisDictionary.CreateFromDictionaries(dictionaries.Concat(s_mainDictionary));
var cancellationToken = context.CancellationToken;

compilationStartContext.RegisterOperationAction(AnalyzeVariable, OperationKind.VariableDeclarator);
compilationStartContext.RegisterCompilationEndAction(AnalyzeAssembly);
compilationStartContext.RegisterSymbolAction(
var dictionaries = ReadDictionaries().Add(s_mainDictionary);

context.RegisterOperationAction(AnalyzeVariable, OperationKind.VariableDeclarator);
context.RegisterCompilationEndAction(AnalyzeAssembly);
context.RegisterSymbolAction(
AnalyzeSymbol,
SymbolKind.Namespace,
SymbolKind.NamedType,
Expand All @@ -255,45 +256,31 @@ private static void OnCompilationStart(CompilationStartAnalysisContext compilati
SymbolKind.Field,
SymbolKind.Parameter);

IEnumerable<CodeAnalysisDictionary> ReadDictionaries()
ImmutableArray<CodeAnalysisDictionary> ReadDictionaries()
{
var fileProvider = AdditionalFileProvider.FromOptions(compilationStartContext.Options);
var fileProvider = AdditionalFileProvider.FromOptions(context.Options);
return fileProvider.GetMatchingFiles(@"(?:dictionary|custom).*?\.(?:xml|dic)$")
.Select(CreateDictionaryFromAdditionalText)
.Select(GetOrCreateDictionaryFromAdditionalText)
.Where(x => x != null)
.ToList();

CodeAnalysisDictionary CreateDictionaryFromAdditionalText(AdditionalText additionalFile)
{
var text = additionalFile.GetText(compilationStartContext.CancellationToken);
var isXml = additionalFile.Path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase);
var provider = isXml ? s_xmlDictionaryProvider : s_dicDictionaryProvider;
.ToImmutableArray();
}

if (!compilationStartContext.TryGetValue(text, provider, out var dictionary))
{
try
{
// Annoyingly (and expectedly), TryGetValue swallows the parsing exception,
// so we have to parse again to get it.
var unused = isXml ? ParseXmlDictionary(text) : ParseDicDictionary(text);
ReportFileParseDiagnostic(additionalFile.Path, "Unknown error");
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
ReportFileParseDiagnostic(additionalFile.Path, ex.Message);
}
}
CodeAnalysisDictionary GetOrCreateDictionaryFromAdditionalText(AdditionalText additionalText)
{
var isXml = additionalText.Path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase);
var provider = isXml ? s_xmlDictionaryProvider : s_dicDictionaryProvider;

return dictionary;
}
var (dictionary, exception) = context.TryGetValue(additionalText.GetText(cancellationToken), provider, out var result)
? result
: default;

void ReportFileParseDiagnostic(string filePath, string message)
if (exception != null)
{
var diagnostic = Diagnostic.Create(FileParseRule, Location.None, filePath, message);
compilationStartContext.RegisterCompilationEndAction(x => x.ReportDiagnostic(diagnostic));
var diagnostic = Diagnostic.Create(FileParseRule, Location.None, additionalText.Path, exception.Message);
context.RegisterCompilationEndAction(x => x.ReportDiagnostic(diagnostic));
}

return dictionary!;
}

void AnalyzeVariable(OperationAnalysisContext operationContext)
Expand Down Expand Up @@ -409,7 +396,9 @@ IEnumerable<string> GetMisspelledWords(string symbolName)
static bool IsWordNumeric(string word) => char.IsDigit(word[0]);

bool IsWordSpelledCorrectly(string word)
=> !projectDictionary.UnrecognizedWords.Contains(word) && projectDictionary.RecognizedWords.Contains(word);
{
return !dictionaries.Any(static (d, word) => d.ContainsUnrecognizedWord(word), word) && dictionaries.Any(static (d, word) => d.ContainsRecognizedWord(word), word);
}
}

/// <summary>
Expand Down Expand Up @@ -468,6 +457,20 @@ private static CodeAnalysisDictionary GetMainDictionary()
return ParseDicDictionary(text);
}

private static (CodeAnalysisDictionary dictionary, Exception? exception) ParseDictionary(SourceText text, bool isXml)
{
try
{
return (isXml ? ParseXmlDictionary(text) : ParseDicDictionary(text), exception: null);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
return (null!, ex);
}
}

private static CodeAnalysisDictionary ParseXmlDictionary(SourceText text)
=> text.Parse(CodeAnalysisDictionary.CreateFromXml);

Expand Down

0 comments on commit 4208547

Please sign in to comment.