private static async Task AssertNoCompilerErrorsAsync(CodeFixProvider codeFix, Solution fixedSolution) { var diagnostics = await Analyze.GetDiagnosticsAsync(fixedSolution).ConfigureAwait(false); var introducedDiagnostics = diagnostics .SelectMany(x => x) .Where(IsIncluded) .ToArray(); if (introducedDiagnostics.Select(x => x.Id) .Except(DiagnosticSettings.AllowedErrorIds()) .Any()) { var errorBuilder = StringBuilderPool.Borrow(); errorBuilder.AppendLine($"{codeFix} introduced syntax error{(introducedDiagnostics.Length > 1 ? "s" : string.Empty)}."); foreach (var introducedDiagnostic in introducedDiagnostics) { var errorInfo = await introducedDiagnostic.ToStringAsync(fixedSolution).ConfigureAwait(false); errorBuilder.AppendLine($"{errorInfo}"); } errorBuilder.AppendLine("First source file with error is:"); var sources = await Task.WhenAll(fixedSolution.Projects.SelectMany(p => p.Documents).Select(d => CodeReader.GetStringFromDocumentAsync(d, Formatter.Annotation, CancellationToken.None))); var lineSpan = introducedDiagnostics.First().Location.GetMappedLineSpan(); var match = sources.SingleOrDefault(x => CodeReader.FileName(x) == lineSpan.Path); errorBuilder.Append(match); errorBuilder.AppendLine(); throw AssertException.Create(errorBuilder.Return()); } }
private static List <string> MergeFixedCodeWithErrorsIndicated(IReadOnlyList <string> codeWithErrorsIndicated, string fixedCode) { var merged = new List <string>(codeWithErrorsIndicated.Count); var found = false; foreach (var code in codeWithErrorsIndicated) { if (code.IndexOf('↓') >= 0) { if (found) { throw AssertException.Create("Expected only one with errors indicated."); } merged.Add(fixedCode); found = true; } else { merged.Add(code); } } if (!found) { throw AssertException.Create("Expected one with errors indicated."); } return(merged); }
private static List <string> MergeFixedCode(IReadOnlyList <string> codes, string fixedCode) { var merged = new List <string>(codes.Count); var found = false; var fileName = CodeReader.FileName(fixedCode); foreach (var code in codes) { if (CodeReader.FileName(code) == fileName) { if (found) { throw AssertException.Create("Expected only one with errors indicated."); } merged.Add(fixedCode); found = true; } else { merged.Add(code); } } if (!found) { throw AssertException.Create("Expected one with errors indicated."); } return(merged); }
/// <summary> /// Check the solution for compiler errors and warnings, uses: /// </summary> public static async Task NoCompilerErrorsAsync(Solution solution, IReadOnlyList <string> allowedIds, AllowedDiagnostics allowedDiagnostics) { var diagnostics = await Analyze.GetDiagnosticsAsync(solution).ConfigureAwait(false); var introducedDiagnostics = diagnostics .SelectMany(x => x) .Where(x => IsIncluded(x, allowedDiagnostics)) .ToArray(); if (introducedDiagnostics.Select(x => x.Id) .Except(allowedIds ?? Enumerable.Empty <string>()) .Any()) { var error = StringBuilderPool.Borrow(); error.AppendLine($"Found error{(introducedDiagnostics.Length > 1 ? "s" : string.Empty)}."); foreach (var introducedDiagnostic in introducedDiagnostics) { var errorInfo = await introducedDiagnostic.ToStringAsync(solution).ConfigureAwait(false); error.AppendLine($"{errorInfo}"); } throw AssertException.Create(StringBuilderPool.Return(error)); } }
private static void AssertCodeFixCanFixDiagnosticsFromAnalyzer(DiagnosticAnalyzer analyzer, CodeFixProvider codeFix) { if (!analyzer.SupportedDiagnostics.Select(d => d.Id).Intersect(codeFix.FixableDiagnosticIds).Any()) { var message = $"Analyzer {analyzer} does not produce diagnostics fixable by {codeFix}.{Environment.NewLine}" + $"The analyzer produces the following diagnostics: {{{string.Join(", ", analyzer.SupportedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", codeFix.FixableDiagnosticIds)}}}"; throw AssertException.Create(message); } }
/// <summary> /// Verifies that <paramref name="code"/> produces no diagnostics when analyzed with <paramref name="analyzer"/>. /// </summary> /// <param name="analyzer">The analyzer.</param> /// <param name="code"> /// The code to create the solution from. /// Can be a .cs, .csproj or .sln file /// </param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/> to use.</param> /// <param name="metadataReferences">The metadata references to use when compiling.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task ValidAsync(DiagnosticAnalyzer analyzer, FileInfo code, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReferences) { var diagnostics = await Analyze.GetDiagnosticsAsync(analyzer, code, compilationOptions, metadataReferences) .ConfigureAwait(false); if (diagnostics.SelectMany(x => x).Any()) { throw AssertException.Create(string.Join(Environment.NewLine, diagnostics.SelectMany(x => x))); } }
/// <summary> /// Verifies that <paramref name="solution"/> produces no diagnostics when analyzed with <paramref name="analyzer"/>. /// </summary> /// <param name="analyzer">The <see cref="DiagnosticAnalyzer"/>.</param> /// <param name="solution">The <see cref="Solution"/> for which no errors or warnings are expected.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task ValidAsync(DiagnosticAnalyzer analyzer, Solution solution) { var diagnostics = await Analyze.GetDiagnosticsAsync(solution, analyzer) .ConfigureAwait(false); if (diagnostics.SelectMany(x => x).Any()) { throw AssertException.Create(string.Join(Environment.NewLine, diagnostics.SelectMany(x => x))); } }
/// <summary> /// Verifies that /// 1. <paramref name="diagnosticsAndSources"/> produces the expected diagnostics when analyzed. /// 2. The code fix fixes the code. /// </summary> /// <param name="analyzer">The analyzer to run on the code..</param> /// <param name="codeFix">The code fix to apply.</param> /// <param name="diagnosticsAndSources">The code and expected diagnostics.</param> /// <param name="fixedCode">The expected code produced by the code fix.</param> /// <param name="fixTitle">The title of the fix to apply if more than one.</param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/> to use.</param> /// <param name="metadataReferences">The meta data metadataReferences to add to the compilation.</param> /// <param name="allowCompilationErrors">If compilation errors are accepted in the fixed code.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task CodeFixAsync(DiagnosticAnalyzer analyzer, CodeFixProvider codeFix, DiagnosticsAndSources diagnosticsAndSources, string fixedCode, string fixTitle, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReferences, AllowCompilationErrors allowCompilationErrors) { AssertCodeFixCanFixDiagnosticsFromAnalyzer(analyzer, codeFix); var data = await DiagnosticsWithMetadataAsync(analyzer, diagnosticsAndSources, compilationOptions, metadataReferences).ConfigureAwait(false); var fixableDiagnostics = data.ActualDiagnostics.SelectMany(x => x) .Where(x => codeFix.FixableDiagnosticIds.Contains(x.Id)) .ToArray(); if (fixableDiagnostics.Length == 0) { var message = $"Code analyzed with {analyzer} did not generate any diagnostics fixable by {codeFix}.{Environment.NewLine}" + $"The analyzed code contained the following diagnostics: {{{string.Join(", ", data.ExpectedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", codeFix.FixableDiagnosticIds)}}}"; throw AssertException.Create(message); } if (fixableDiagnostics.Length > 1) { var message = $"Code analyzed with {analyzer} generated more than one diagnostic fixable by {codeFix}.{Environment.NewLine}" + $"The analyzed code contained the following diagnostics: {{{string.Join(", ", data.ExpectedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", codeFix.FixableDiagnosticIds)}}}{Environment.NewLine}" + $"Maybe you meant to call AnalyzerAssert.FixAll?"; throw AssertException.Create(message); } var diagnostic = fixableDiagnostics.Single(); var fixedSolution = await Fix.ApplyAsync(data.Solution, codeFix, diagnostic, fixTitle, CancellationToken.None).ConfigureAwait(false); if (ReferenceEquals(data.Solution, fixedSolution)) { throw AssertException.Create($"{codeFix} did not change any document."); } var fixedSource = await CodeReader.GetStringFromDocumentAsync( fixedSolution.GetDocument(data.Solution.GetDocument(diagnostic.Location.SourceTree).Id), Formatter.Annotation, CancellationToken.None) .ConfigureAwait(false); CodeAssert.AreEqual(fixedCode, fixedSource); if (allowCompilationErrors == AllowCompilationErrors.No) { await AssertNoCompilerErrorsAsync(codeFix, fixedSolution).ConfigureAwait(false); } }
/// <summary> /// Verifies that <paramref name="code"/> produces no diagnostics when analyzed with <paramref name="analyzer"/>. /// </summary> /// <param name="analyzer">The analyzer.</param> /// <param name="code">The code to analyze.</param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/> to use.</param> /// <param name="metadataReferences">The metadata references to use when compiling.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task ValidAsync(DiagnosticAnalyzer analyzer, IReadOnlyList <string> code, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReferences) { var diagnostics = await Analyze.GetDiagnosticsAsync( analyzer, code, compilationOptions, metadataReferences) .ConfigureAwait(false); if (diagnostics.SelectMany(x => x).Any()) { var builder = StringBuilderPool.Borrow().AppendLine("Expected no diagnostics, found:"); foreach (var diagnostic in diagnostics.SelectMany(x => x)) { builder.AppendLine(diagnostic.ToString(code)); } throw AssertException.Create(builder.Return()); } }
private static async Task <DiagnosticsMetadata> CreateDiagnosticsMetadataAsync(DiagnosticAnalyzer analyzer, CodeFixProvider codeFix, DiagnosticsAndSources diagnosticsAndSources, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReference) { AssertCodeFixCanFixDiagnosticsFromAnalyzer(analyzer, codeFix); var data = await DiagnosticsWithMetadataAsync(analyzer, diagnosticsAndSources, compilationOptions, metadataReference) .ConfigureAwait(false); var fixableDiagnostics = data.ActualDiagnostics.SelectMany(x => x) .Where(x => codeFix.FixableDiagnosticIds.Contains(x.Id)) .ToArray(); if (fixableDiagnostics.Length == 0) { var message = $"Code analyzed with {analyzer} did not generate any diagnostics fixable by {codeFix}.{Environment.NewLine}" + $"The analyzed code contained the following diagnostics: {{{string.Join(", ", data.ExpectedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", codeFix.FixableDiagnosticIds)}}}"; throw AssertException.Create(message); } return(data); }
/// <summary> /// Verifies that /// 1. <paramref name="diagnosticsAndSources"/> produces the expected diagnostics /// 2. The code fix does not change the code. /// </summary> /// <param name="analyzer">The type of the analyzer.</param> /// <param name="codeFix">The type of the code fix.</param> /// <param name="diagnosticsAndSources">The code with error positions indicated.</param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/> to use.</param> /// <param name="metadataReferences">The meta data references to use when compiling.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task NoFixAsync(DiagnosticAnalyzer analyzer, CodeFixProvider codeFix, DiagnosticsAndSources diagnosticsAndSources, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReferences) { AssertCodeFixCanFixDiagnosticsFromAnalyzer(analyzer, codeFix); var data = await DiagnosticsWithMetadataAsync(analyzer, diagnosticsAndSources, compilationOptions, metadataReferences).ConfigureAwait(false); var fixableDiagnostics = data.ActualDiagnostics.SelectMany(x => x) .Where(x => codeFix.FixableDiagnosticIds.Contains(x.Id)) .ToArray(); if (fixableDiagnostics.Length != 1) { throw AssertException.Create("Expected code to have exactly one fixable diagnostic."); } if (await Fix.IsRegisteringFixAsync(data.Solution, codeFix, fixableDiagnostics.Single())) { var fixedSolution = await Fix.ApplyAsync(data.Solution, codeFix, fixableDiagnostics.Single(), null, CancellationToken.None) .ConfigureAwait(false); await AreEqualAsync(data.Sources, fixedSolution, "Expected the code fix to not change any document.").ConfigureAwait(false); } }
private static void AssertAnalyzerSupportsExpectedDiagnostic(DiagnosticAnalyzer analyzer, ExpectedDiagnostic expectedDiagnostic, out DiagnosticDescriptor descriptor, out IReadOnlyList <string> suppressedDiagnostics) { var descriptors = analyzer.SupportedDiagnostics.Where(x => x.Id == expectedDiagnostic.Id).ToArray(); if (descriptors.Length == 0) { var message = $"Analyzer {analyzer} does not produce a diagnostic with ID {expectedDiagnostic.Id}.{Environment.NewLine}" + $"The analyzer produces the following diagnostics: {{{string.Join(", ", analyzer.SupportedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The expected diagnostic is: {expectedDiagnostic.Id}"; throw AssertException.Create(message); } if (descriptors.Length > 1) { var message = $"Analyzer {analyzer} supports multiple diagnostics with ID {expectedDiagnostic.Id}.{Environment.NewLine}" + $"The analyzer produces the following diagnostics: {{{string.Join(", ", analyzer.SupportedDiagnostics.Select(d => d.Id))}}}{Environment.NewLine}" + $"The expected diagnostic is: {expectedDiagnostic.Id}"; throw AssertException.Create(message); } suppressedDiagnostics = analyzer.SupportedDiagnostics.Select(x => x.Id).Where(x => x != expectedDiagnostic.Id).ToArray(); descriptor = descriptors[0]; }
/// <summary> /// Verify that two strings of code are equal. Agnostic to end of line characters. /// </summary> /// <param name="expected">The expected code.</param> /// <param name="actual">The actual code.</param> /// <param name="messageHeader">The first line to add to the exception message on error.</param> internal static void AreEqual(string expected, string actual, string messageHeader) { var pos = 0; var otherPos = 0; var line = 1; while (pos < expected.Length && otherPos < actual.Length) { if (expected[pos] == '\r') { pos++; continue; } if (actual[otherPos] == '\r') { otherPos++; continue; } if (expected[pos] != actual[otherPos]) { var errorBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { errorBuilder.AppendLine(messageHeader); } errorBuilder.AppendLine($"Mismatch on line {line}"); var expectedLine = expected.Split('\n')[line - 1].Trim('\r'); var actualLine = actual.Split('\n')[line - 1].Trim('\r'); var diffPos = Math.Min(expectedLine.Length, actualLine.Length); for (var i = 0; i < Math.Min(expectedLine.Length, actualLine.Length); i++) { if (expectedLine[i] != actualLine[i]) { diffPos = i; break; } } errorBuilder.AppendLine($"Expected: {expectedLine}"); errorBuilder.AppendLine($"Actual: {actualLine}"); errorBuilder.AppendLine($" {new string(' ', diffPos)}^"); throw AssertException.Create(errorBuilder.Return()); } if (expected[pos] == '\n') { line++; } pos++; otherPos++; } while (pos < expected.Length && expected[pos] == '\r') { pos++; } while (otherPos < actual.Length && actual[otherPos] == '\r') { otherPos++; } if (pos == expected.Length && otherPos == actual.Length) { return; } if (messageHeader != null) { throw AssertException.Create($"{messageHeader}{Environment.NewLine}" + $"Mismatch at end of text."); } throw AssertException.Create($"Mismatch at end of text."); }
/// <summary> /// Verifies that <paramref name="sources"/> produces the expected diagnostics. /// </summary> /// <param name="analyzer">The analyzer to apply.</param> /// <param name="sources">The code with error positions indicated.</param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/> to use.</param> /// <param name="metadataReferences">The meta data metadataReferences to use when compiling.</param> /// <param name="expectedMessage">The expected message in the diagnostic produced by the analyzer.</param> /// <returns>The meta data from the run..</returns> public static async Task <DiagnosticsMetadata> DiagnosticsWithMetadataAsync( DiagnosticAnalyzer analyzer, DiagnosticsAndSources sources, CSharpCompilationOptions compilationOptions, IReadOnlyList <MetadataReference> metadataReferences, string expectedMessage = null) { if (sources.ExpectedDiagnostics.Count == 0) { throw AssertException.Create("Expected code to have at least one error position indicated with '↓'"); } var data = await Analyze.GetDiagnosticsWithMetadataAsync( analyzer, sources.Code, compilationOptions, metadataReferences) .ConfigureAwait(false); var expecteds = sources.ExpectedDiagnostics; var actuals = data.Diagnostics .SelectMany(x => x) .ToArray(); if (expecteds.SetEquals(actuals)) { if (expectedMessage != null) { foreach (var actual in data.Diagnostics.SelectMany(x => x)) { var actualMessage = actual.GetMessage(CultureInfo.InvariantCulture); TextAssert.AreEqual(expectedMessage, actualMessage, $"Expected and actual diagnostic message for the diagnostic {actual} does not match"); } } return(new DiagnosticsMetadata( sources.Code, sources.ExpectedDiagnostics, data.Diagnostics, data.Solution)); } var error = StringBuilderPool.Borrow(); error.AppendLine("Expected and actual diagnostics do not match."); var missingExpected = expecteds.Except(actuals); for (var i = 0; i < missingExpected.Count; i++) { if (i == 0) { error.Append("Expected:\r\n"); } var expected = missingExpected[i]; error.AppendLine(expected.ToString(sources.Code)); } if (actuals.Length == 0) { error.AppendLine("Actual: <no errors>"); } var missingActual = actuals.Except(expecteds); if (actuals.Length > 0 && missingActual.Count == 0) { error.AppendLine("Actual: <missing>"); } for (var i = 0; i < missingActual.Count; i++) { if (i == 0) { error.Append("Actual:\r\n"); } var actual = missingActual[i]; error.AppendLine(actual.ToString(sources.Code)); } throw AssertException.Create(StringBuilderPool.Return(error)); }
/// <summary> /// Verify that two strings of code are equal. Agnostic to end of line characters. /// </summary> /// <param name="expected">The expected code.</param> /// <param name="actual">The actual code.</param> /// <param name="messageHeader">The first line to add to the exception message on error.</param> internal static void AreEqual(string expected, string actual, string messageHeader) { var expectedPos = 0; var actualPos = 0; var line = 1; while (expectedPos < expected.Length && actualPos < actual.Length) { var ec = expected[expectedPos]; var ac = actual[actualPos]; if (ec == '\r' || ac == '\r') { if (ec == '\r') { expectedPos++; } if (ac == '\r') { actualPos++; } continue; } if (ec != ac) { var errorBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { errorBuilder.AppendLine(messageHeader); } errorBuilder.AppendLine($"Mismatch on line {line} of file {CodeReader.FileName(expected)}"); var expectedLine = expected.Split('\n')[line - 1].Trim('\r'); var actualLine = actual.Split('\n')[line - 1].Trim('\r'); var diffPos = Math.Min(expectedLine.Length, actualLine.Length); for (var i = 0; i < Math.Min(expectedLine.Length, actualLine.Length); i++) { if (expectedLine[i] != actualLine[i]) { diffPos = i; break; } } errorBuilder.AppendLine($"Expected: {expectedLine}") .AppendLine($"Actual: {actualLine}") .AppendLine($" {new string(' ', diffPos)}^") .AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine(); throw AssertException.Create(errorBuilder.Return()); } if (ec == '\n') { line++; } expectedPos++; actualPos++; } while (expectedPos < expected.Length && expected[expectedPos] == '\r') { expectedPos++; } while (actualPos < actual.Length && actual[actualPos] == '\r') { actualPos++; } if (expectedPos == expected.Length && actualPos == actual.Length) { return; } if (messageHeader != null) { var message = StringBuilderPool.Borrow() .AppendLine(messageHeader) .AppendLine($"Mismatch at end of file {CodeReader.FileName(expected)}") .AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine() .Return(); throw AssertException.Create(message); } else { var message = StringBuilderPool.Borrow() .AppendLine($"Mismatch at end of file {CodeReader.FileName(expected)}") .AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine() .Return(); throw AssertException.Create(message); } }