/// <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); } }