/// <summary> /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. /// The returned diagnostics are then ordered by location in the source document. /// </summary> /// <param name="analyzer">The analyzer to run on the documents.</param> /// <param name="documents">The <see cref="Document">Documents</see> that the analyzer will be run on.</param> /// <param name="validationModes">Flags that specify which compilation diagnostics can cause a failure.</param> /// <param name="additionalFiles">Enumerable of <see cref="AdditionalText" /> that will appear to the analyzer as additional files.</param> /// <returns>An <see cref="IEnumerable{Diagnostic}" /> that surfaced in the source code, sorted by Location.</returns> private static async Task <IEnumerable <Diagnostic> > GetSortedDiagnosticsFromDocumentsAsync( DiagnosticAnalyzer analyzer, IEnumerable <Document> documents, TestValidationModes validationModes, IEnumerable <AdditionalText> additionalFiles, Dictionary <string, IEnumerable <AdditionalText> > projectAdditionalFiles) { var projects = new HashSet <Project>(); foreach (var document in documents) { projects.Add(document.Project); } var relevantDiagnostics = new List <Diagnostic>(); foreach (var project in projects) { var compilation = await project.GetCompilationAsync().ConfigureAwait(false); compilation = EnableDiagnostics(analyzer, compilation); if (validationModes != TestValidationModes.None) { ValidateCompilation(compilation, validationModes); } var allProjectsFiles = additionalFiles ?? Enumerable.Empty <AdditionalText>(); var allAdditionalFiles = projectAdditionalFiles != null && projectAdditionalFiles.TryGetValue(project.Name, out var thisProjectsFiles) ? allProjectsFiles.Union(thisProjectsFiles) : allProjectsFiles; var analyzerOptions = new AnalyzerOptions(ImmutableArray.Create(allAdditionalFiles.ToArray())); var compilationWithAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer), analyzerOptions); var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false); foreach (var diagnostic in diagnostics) { if (diagnostic.Location == Location.None || diagnostic.Location.IsInMetadata) { relevantDiagnostics.Add(diagnostic); } else { foreach (var document in documents) { var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false); if (tree == diagnostic.Location.SourceTree) { relevantDiagnostics.Add(diagnostic); } } } } } return(SortDiagnostics(relevantDiagnostics)); }
/// <summary> /// Returns the diagnostics found in the given <paramref name="testFiles"/> for the given <paramref name="analyzer"/>. /// </summary> /// <param name="analyzer">The analyzer to be run on the sources.</param> /// <param name="documents">The <see cref="Document">Documents</see> that the analyzer will be run on.</param> /// <param name="validationModes">Flags that specify which compilation diagnostics can cause a failure.</param> /// <param name="additionalFiles">Enumerable of <see cref="AdditionalText"/> that will appear to the analyzer as additional files.</param> /// <param name="projectAdditionalFiles">Mapping of project name to the array of <see cref="AdditionalText"/> it includes.</param> /// <returns>An <see cref="IEnumerable{Diagnostic}" /> that surfaced in the source code, sorted by Location.</returns> protected static async Task <IEnumerable <Diagnostic> > GetSortedDiagnosticsAsync( DiagnosticAnalyzer analyzer, IEnumerable <Document> documents, TestValidationModes validationModes = DefaultTestValidationMode, IEnumerable <AdditionalText> additionalFiles = null, Dictionary <string, IEnumerable <AdditionalText> > projectAdditionalFiles = null) { return(await GetSortedDiagnosticsFromDocumentsAsync( analyzer, documents, validationModes, additionalFiles, projectAdditionalFiles).ConfigureAwait(false)); }
/// <summary> /// Ensures no compilation diagnostics exist with severities that match what's specified in /// <paramref name="validationModes"/> /// </summary> /// <remarks> /// The idea is taken from the Roslyn Analyzers' project, though their implementation differs greatly (different APIs): /// https://github.com/dotnet/roslyn-analyzers/blob/569bd373a4831d3035597197e02980b57602a7f2/src/Test/Utilities/DiagnosticExtensions.cs /// </remarks> /// <param name="compilation">An object that allows us to obtain the warnings, errors, ... from a compilation</param> /// <param name="validationModes">Flags that specify which compilation diagnostics can cause a failure</param> private static void ValidateCompilation(Compilation compilation, TestValidationModes validationModes) { // Ignore the diagnostics about missing a main method (CS5001, BC30420) or not being able to find a type/namespace (CS0246) // Ids taken from: https://github.com/Vannevelj/RoslynTester/blob/master/RoslynTester/RoslynTester/Helpers/DiagnosticVerifier.cs var ignoredIds = new HashSet <string>(new[] { "CS5001", "BC30420", "CS0246" }); var severityMapping = new Dictionary <DiagnosticSeverity, TestValidationModes> { { DiagnosticSeverity.Error, TestValidationModes.ValidateErrors }, { DiagnosticSeverity.Warning, TestValidationModes.ValidateWarnings }, }; // Create a mapping from TestValidationMode to a list of compilation diagnostics. If no // compilation diagnostics exist for a given TestValidationMode, no entry is created var compileDiagnostics = compilation.GetDiagnostics() .Where(diagnostic => !ignoredIds.Contains(diagnostic.Id) && severityMapping.ContainsKey(diagnostic.Severity) && validationModes.HasFlag(severityMapping[diagnostic.Severity])) .GroupBy(diagnostic => diagnostic.Severity) .Select(g => new { Key = severityMapping[g.Key], Values = g.ToList() }) .ToDictionary(g => g.Key, g => g.Values); if (compileDiagnostics.Keys.Any()) { // Helper function that returns two different strings. The first looks like "1 error(s)" and the // second like "TestValidationModes.ValidateErrors". Both will be used to construct a helpful message var getMessageParts = new Func <TestValidationModes, string, Tuple <string, string> >((mode, whats) => { return(validationModes.HasFlag(mode) && compileDiagnostics.ContainsKey(mode) ? new Tuple <string, string>( string.Format(CultureInfo.CurrentCulture, "{0} {1}", compileDiagnostics[mode].Count, whats), string.Format(CultureInfo.CurrentCulture, "{0}.{1}", nameof(TestValidationModes), Enum.GetName(typeof(TestValidationModes), mode))) : null); }); var messages = new[] { getMessageParts(TestValidationModes.ValidateErrors, "error(s)"), getMessageParts(TestValidationModes.ValidateWarnings, "warning(s)"), }.Where(x => x != null).ToArray(); // Create the helpful message var message = string.Format( CultureInfo.CurrentCulture, "\nTest compilation contains {0}. Disable {1} if these are expected:\n", string.Join(", ", messages.Select(x => x.Item1)), string.Join(" and/or ", messages.Select(x => x.Item2))); var builder = new StringBuilder(message); foreach (TestValidationModes mode in Enum.GetValues(typeof(TestValidationModes))) { if (compileDiagnostics.ContainsKey(mode)) { builder.Append(string.Format(CultureInfo.CurrentCulture, "\n {0}", string.Join("\n", compileDiagnostics[mode]))); } } Xunit.Assert.True(false, builder.ToString()); } }