/// <summary> /// Helper method to VerifyDiagnosticResult that checks the location of /// a diagnostic and compares it with the location in the expected /// DiagnosticResult. /// </summary> /// <param name="analyzer"> /// The analyzer that was being run on the sources. /// </param> /// <param name="atmosphere"> /// The compilation environment. /// </param> /// <param name="diagnostic"> /// The diagnostic that was found in the code. /// </param> /// <param name="actual"> /// The Location of the Diagnostic found in the code. /// </param> /// <param name="expected"> /// The DiagnosticResultLocation that should have been found. /// </param> private static void VerifyDiagnosticLocation( DiagnosticAnalyzer analyzer, Atmosphere atmosphere, Diagnostic diagnostic, Location actual, ResultLocation expected) { string Message() => FormatDiagnostics( analyzer, atmosphere, diagnostic); var actualSpan = actual.GetLineSpan(); var actualLinePosition = actualSpan.StartLinePosition; AssertFailIfFalse( actualSpan.Path == expected.Path || (!(actualSpan.Path is null) && !(expected.Path is null) && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), () => $"Expected diagnostic to be in file '{expected.Path}' " + "was actually in file " + $"'{actualSpan.Path}'{NewLine}" + $"{NewLine}" + $"Diagnostic:{NewLine}" + $" {Message()}{NewLine}"); // Only check line position if there is an actual line in the real // diagnostic AssertFailIfTrue( actualLinePosition.Line >= 0 && actualLinePosition.Line + 1 != expected.Line, () => "Expected diagnostic to be on line " + $"'{expected.Line}' was actually on line " + $"'{actualLinePosition.Line + 1}'{NewLine}" + $"{NewLine}" + $"Diagnostic:{NewLine}" + $" {Message()}{NewLine}"); // Only check column position if there is an actual column position // in the real diagnostic AssertFailIfTrue( actualLinePosition.Character >= 0 && actualLinePosition.Character + 1 != expected.Column, () => "Expected diagnostic to start at column " + $"'{expected.Column}' was actually at column " + $"'{actualLinePosition.Character + 1}'{NewLine}" + $"{NewLine}" + $"Diagnostic:{NewLine}" + $" {Message()}{NewLine}"); }
/// <summary> /// Tests the analyzer. Verifies each of diagnostics found in the /// specified source, compared with the result the specified function /// extracts from the beliefs embedded from the source. /// </summary> /// <param name="encodedSource"> /// The encoded source where the beliefs have been embedded. /// </param> /// <param name="atmosphere"> /// The compilation environment. /// </param> /// <param name="toResult"> /// The function that returns the expected diagnostic result with the /// specified belief. /// </param> protected void VerifyDiagnostic( string encodedSource, Atmosphere atmosphere, Func <Belief, Result> toResult) { var(source, expected) = Beliefs.Decode( encodedSource, atmosphere.ExcludeIds, toResult); try { VerifyDiagnostics(atmosphere, expected, source); } catch (CompilationException) { Trace.WriteLine(source); throw; } }
/// <summary> /// Creates a project using the specified code suppliers. /// </summary> /// <typeparam name="T"> /// The type that supplies a source. /// </typeparam> /// <param name="atmosphere"> /// The compilation environment. /// </param> /// <param name="codeSuppliers"> /// The suppliers to supply sources. /// </param> /// <param name="toString"> /// The function that consumes a code supplier and then returns a /// source in the form of a <c>string</c>. /// </param> /// <param name="notifyDocumentId"> /// The function that consumes a <c>DocumentId</c> object and the code /// supplier. /// </param> /// <returns> /// The new project. /// </returns> private static Project CreateProject <T>( Atmosphere atmosphere, IEnumerable <T> codeSuppliers, Func <T, string> toString, Action <DocumentId, T> notifyDocumentId) { var basePath = atmosphere.BasePath; var projectId = ProjectId.CreateNewId(TestProjectName); var solution = new AdhocWorkspace() .CurrentSolution .AddProject( projectId, TestProjectName, TestProjectName, LanguageNames.CSharp) .AddMetadataReferences(projectId, AllReferences); var codeSupplierArray = codeSuppliers.ToArray(); var n = codeSupplierArray.Length; for (var k = 0; k < n; ++k) { var codeSupplier = codeSupplierArray[k]; var source = toString(codeSupplier); var newFileName = $"{DefaultFilePathPrefix}{k}.{CSharpDefaultFileExt}"; var path = basePath is null ? newFileName : Path.Combine(basePath, newFileName); var documentId = DocumentId.CreateNewId( projectId, debugName: path); solution = solution.AddDocument( documentId, path, SourceText.From(source)); notifyDocumentId(documentId, codeSupplier); } var project = solution.GetProject(projectId) ?? throw new NullReferenceException(); var parseOption = project.ParseOptions ?? throw new NullReferenceException(); parseOption = parseOption.WithDocumentationMode( atmosphere.DocumentationMode); return(project.WithParseOptions(parseOption)); }
/// <summary> /// Verifies each of diagnostics found in the specified sources with /// the specified analyzer, compared with the specified expected /// result. /// </summary> /// <param name="atmosphere"> /// The compilation environment. /// </param> /// <param name="expected"> /// The expected results that should appear after the analyzer is run /// on the sources. /// </param> /// <param name="sources"> /// Strings to create source documents from to run the analyzers on. /// </param> protected void VerifyDiagnostics( Atmosphere atmosphere, IEnumerable <Result> expected, params string[] sources) { var analyzer = DiagnosticAnalyzer; var documents = Projects.Of(atmosphere, sources) .Documents .ToArray(); if (sources.Length != documents.Length) { throw new InvalidOperationException( "Amount of sources did not match amount of Documents " + "created"); } var diagnostics = Diagnostics.GetSorted( analyzer, documents, atmosphere); VerifyDiagnosticResults( diagnostics, expected, analyzer, atmosphere); }
/// <summary> /// Helper method to format a Diagnostic into an easily readable /// string. /// </summary> /// <param name="analyzer"> /// The analyzer that this verifier tests. /// </param> /// <param name="atmosphere"> /// The compilation environment. /// </param> /// <param name="diagnostics"> /// The Diagnostics to be formatted. /// </param> /// <returns> /// The Diagnostics formatted as a string. /// </returns> private static string FormatDiagnostics( DiagnosticAnalyzer analyzer, Atmosphere atmosphere, params Diagnostic[] diagnostics) { var builder = new StringBuilder(); for (var i = 0; i < diagnostics.Length; ++i) { builder.Append("// ") .AppendLine(diagnostics[i].ToString()); var analyzerType = analyzer.GetType(); var rule = analyzer.SupportedDiagnostics .FirstOrDefault(r => !(r is null) && r.Id == diagnostics[i].Id); if (rule is null) { continue; } var location = diagnostics[i].Location; if (location == Location.None) { builder.AppendFormat( CultureInfo.CurrentCulture, "GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); } else if (atmosphere.ForceLocationValid) { var linePosition = diagnostics[i].Location .GetLineSpan() .StartLinePosition; builder.AppendFormat( CultureInfo.CurrentCulture, "GetExternalResult({0}, {1}, {2}.{3})", linePosition.Line + 1, linePosition.Character + 1, analyzerType.Name, rule.Id); } else { AssertFailIfFalse( location.IsInSource, () => "Test base does not currently handle " + "diagnostics in metadata locations. " + "Diagnostic in metadata: " + $"{diagnostics[i]}{NewLine}"); var sourceTree = diagnostics[i].Location .SourceTree; if (sourceTree is null) { throw new NullReferenceException(); } var filePath = sourceTree.FilePath; AssertFailIfFalse( filePath.EndsWith(".cs", StringComparison.Ordinal), () => "The file path does not end '.cs': " + $"{filePath}"); var resultMethodName = "GetCSharpResultAt"; var linePosition = diagnostics[i].Location .GetLineSpan() .StartLinePosition; builder.AppendFormat( CultureInfo.CurrentCulture, "{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(); } return(builder.ToString()); }
/// <summary> /// 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. /// </summary> /// <param name="actualDiagnostics"> /// The Diagnostics found by the compiler after running the analyzer on /// the source code. /// </param> /// <param name="expectedDiagnostics"> /// Diagnostic Results that should have appeared in the code. /// </param> /// <param name="analyzer"> /// The analyzer that was being run on the sources. /// </param> /// <param name="atmosphere"> /// The compilation environment. /// </param> private static void VerifyDiagnosticResults( IEnumerable <Diagnostic> actualDiagnostics, IEnumerable <Result> expectedDiagnostics, DiagnosticAnalyzer analyzer, Atmosphere atmosphere) { var actualResults = actualDiagnostics.ToArray(); var expectedResults = expectedDiagnostics.ToArray(); var expectedCount = expectedResults.Length; var actualCount = actualResults.Length; string DiagnosticsOutput() => actualResults.Length > 0 ? FormatDiagnostics(analyzer, atmosphere, actualResults) : " NONE."; AssertFailIfFalse( expectedCount == actualCount, () => "Mismatch between number of diagnostics returned, " + $"expected '{expectedCount}' " + $"actual '{actualCount}'{NewLine}" + $"{NewLine}" + $"Diagnostics:{NewLine}" + $"{DiagnosticsOutput()}{NewLine}"); for (var i = 0; i < expectedResults.Length; ++i) { var actual = actualResults[i]; var expected = expectedResults[i]; string Message() => FormatDiagnostics( analyzer, atmosphere, actual); if (expected.Line == -1 && expected.Column == -1) { AssertFailIfFalse( Location.None.Equals(actual.Location), () => $"Expected:{NewLine}" + $"A project diagnostic with No location{NewLine}" + $"Actual:{NewLine}" + $"{Message()}"); } else { VerifyDiagnosticLocation( analyzer, atmosphere, actual, actual.Location, expected.Locations[0]); var additionalLocations = actual.AdditionalLocations.ToArray(); var expectedAdditionalLocations = expected.Locations.Length - 1; var actualAdditionalLocations = additionalLocations.Length; AssertFailIfFalse( expectedAdditionalLocations == actualAdditionalLocations, () => $"Expected " + $"{expectedAdditionalLocations} " + $"additional locations but got " + $"{actualAdditionalLocations} for " + $"Diagnostic:{NewLine}" + $" {Message()}{NewLine}"); for (var j = 0; j < additionalLocations.Length; ++j) { VerifyDiagnosticLocation( analyzer, atmosphere, actual, additionalLocations[j], expected.Locations[j + 1]); } } void AssertOne <T>( string label, T expectedValue, T actualValue) where T : notnull { if (expectedValue.Equals(actualValue)) { return; } Assert.Fail( $"Expected diagnostic {label} to be " + $"'{expectedValue}' was " + $"'{actualValue}'{NewLine}" + $"{NewLine}" + $"Diagnostic:{NewLine}" + $" {Message()}{NewLine}"); } AssertOne("ID", expected.Id, actual.Id); AssertOne("severity", expected.Severity, actual.Severity); AssertOne("message", expected.Message, actual.GetMessage()); } }
/// <summary> /// Creates a project using the specified strings as sources. /// </summary> /// <param name="atmosphere"> /// The compliation environment. /// </param> /// <param name="sources"> /// Classes in the form of strings. /// </param> /// <returns> /// A Project created out of the Documents created from the source /// strings. /// </returns> public static Project Of( Atmosphere atmosphere, params string[] sources) { return(CreateProject( atmosphere, sources, s => s, (id, s) => { })); }