using System;
using System.Globalization;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Linq;
using System.Text;
using Xunit;
using Amazon.JSII.Analyzers.UnitTests.Helpers;
namespace Amazon.JSII.Analyzers.UnitTests.Verifiers
{
///
/// Superclass of all Unit Tests for DiagnosticAnalyzers
///
public abstract partial class DiagnosticVerifier
{
#region To be implemented by Test classes
///
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
///
protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
#endregion
#region Verifier wrappers
///
/// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
///
/// A class in the form of a string to run the analyzer on
/// DiagnosticResults that should appear after the analyzer is run on the source
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
{
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
}
///
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
/// then verifies each of them.
///
/// An array of strings to create source documents from to run the analyzers on
/// The language of the classes represented by the source strings
/// The analyzer to be run on the source code
/// DiagnosticResults that should appear after the analyzer is run on the sources
private static void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
{
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
VerifyDiagnosticResults(diagnostics, analyzer, expected);
}
#endregion
#region Actual comparisons and verifications
///
/// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
/// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
///
/// The Diagnostics found by the compiler after running the analyzer on the source code
/// The analyzer that was being run on the sources
/// Diagnostic Results that should have appeared in the code
private static void VerifyDiagnosticResults(Diagnostic[] actualResults, DiagnosticAnalyzer? analyzer, params DiagnosticResult[] expectedResults)
{
var expectedCount = expectedResults.Length;
var actualCount = actualResults.Length;
if (expectedCount != actualCount)
{
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
Assert.Fail($"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"\r\n\r\nDiagnostics:\r\n{diagnosticsOutput}\r\n");
}
for (var i = 0; i < expectedResults.Length; i++)
{
var actual = actualResults.ElementAt(i);
var expected = expectedResults[i];
if (expected.Line == -1 && expected.Column == -1)
{
if (actual.Location != Location.None)
{
Assert.Fail($"Expected:\nA project diagnostic with No location\nActual:\n{FormatDiagnostics(analyzer, actual)}");
}
}
else
{
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations[0]);
var additionalLocations = actual.AdditionalLocations.ToArray();
if (additionalLocations.Length != expected.Locations.Count - 1)
{
Assert.Fail($"Expected {expected.Locations.Count - 1} additional locations but got {additionalLocations.Length} for Diagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n");
}
for (int j = 0; j < additionalLocations.Length; ++j)
{
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
}
}
if (actual.Id != expected.Id)
{
Assert.Fail($"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.Id}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n");
}
if (actual.Severity != expected.Severity)
{
Assert.Fail($"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n");
}
if (actual.GetMessage(CultureInfo.InvariantCulture) != expected.Message)
{
Assert.Fail($"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.GetMessage(CultureInfo.InvariantCulture)}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n");
}
}
}
///
/// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
///
/// The analyzer that was being run on the sources
/// The diagnostic that was found in the code
/// The Location of the Diagnostic found in the code
/// The DiagnosticResultLocation that should have been found
private static void VerifyDiagnosticLocation(DiagnosticAnalyzer? analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
{
var actualSpan = actual.GetLineSpan();
Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.", StringComparison.InvariantCulture) && expected.Path.Contains("Test.", StringComparison.InvariantCulture)),
$"Expected diagnostic to be in file \"{expected.Path}\" was actually in file \"{actualSpan.Path}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n");
var actualLinePosition = actualSpan.StartLinePosition;
// Only check line position if there is an actual line in the real diagnostic
if (actualLinePosition.Line > 0)
{
if (actualLinePosition.Line + 1 != expected.Line)
{
Assert.Fail($"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n");
}
}
// Only check column position if there is an actual column position in the real diagnostic
if (actualLinePosition.Character > 0)
{
if (actualLinePosition.Character + 1 != expected.Column)
{
Assert.Fail($"Expected diagnostic to start at column \"{expected.Column}\" was actually at column \"{actualLinePosition.Character + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n");
}
}
}
#endregion
#region Formatting Diagnostics
///
/// Helper method to format a Diagnostic into an easily readable string
///
/// The analyzer that this verifier tests
/// The Diagnostics to be formatted
/// The Diagnostics formatted as a string
private static string FormatDiagnostics(DiagnosticAnalyzer? analyzer, params Diagnostic[] diagnostics)
{
var builder = new StringBuilder();
for (var i = 0; i < diagnostics.Length; ++i)
{
builder.AppendLine(CultureInfo.InvariantCulture, $"// {diagnostics[i]}");
var analyzerType = analyzer?.GetType();
if (analyzerType == null)
{
continue;
}
var rules = analyzer!.SupportedDiagnostics;
foreach (var rule in rules)
{
if (rule == null || rule.Id != diagnostics[i].Id) continue;
var location = diagnostics[i].Location;
if (location == Location.None)
{
builder.AppendFormat(CultureInfo.InvariantCulture, "GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
}
else
{
Assert.True(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
var fileIsCSharp = diagnostics[i].Location.SourceTree?.FilePath.EndsWith(".cs", StringComparison.InvariantCulture) ?? false;
var resultMethodName = fileIsCSharp ? "GetCSharpResultAt" : "GetBasicResultAt";
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
builder.AppendFormat(CultureInfo.InvariantCulture, "{0}({1}, {2}, {3}.{4})",
resultMethodName,
linePosition.Line + 1,
linePosition.Character + 1,
analyzerType.Name,
rule.Id);
}
if (i != diagnostics.Length - 1)
{
builder.Append(',');
}
builder.AppendLine();
break;
}
}
return builder.ToString();
}
#endregion
}
}