private static async Task VerifyFixAsync(Solution sln, IReadOnlyList <ImmutableArray <Diagnostic> > diagnostics, DiagnosticAnalyzer analyzer, CodeFixProvider fix, string fixedCode, string fixTitle = null, AllowCompilationErrors allowCompilationErrors = AllowCompilationErrors.No) { var fixableDiagnostics = diagnostics.SelectMany(x => x) .Where(x => fix.FixableDiagnosticIds.Contains(x.Id)) .ToArray(); if (fixableDiagnostics.Length == 0) { var message = $"Code analyzed with {analyzer} did not generate any diagnostics fixable by {fix}.{Environment.NewLine}" + $"The analyzed code contained the following diagnostics: {{{string.Join(", ", diagnostics.SelectMany(x => x).Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", fix.FixableDiagnosticIds)}}}"; throw new AssertException(message); } if (fixableDiagnostics.Length > 1) { var message = $"Code analyzed with {analyzer} generated more than one diagnostic fixable by {fix}.{Environment.NewLine}" + $"The analyzed code contained the following diagnostics: {{{string.Join(", ", diagnostics.SelectMany(x => x).Select(d => d.Id))}}}{Environment.NewLine}" + $"The code fix supports the following diagnostics: {{{string.Join(", ", fix.FixableDiagnosticIds)}}}{Environment.NewLine}" + $"Maybe you meant to call AnalyzerAssert.FixAll?"; throw new AssertException(message); } var diagnostic = fixableDiagnostics.Single(); var fixedSolution = await Fix.ApplyAsync(sln, fix, diagnostic, fixTitle).ConfigureAwait(false); if (ReferenceEquals(sln, fixedSolution)) { throw new AssertException($"{fix} did not change any document."); } var fixedSource = await CodeReader.GetStringFromDocumentAsync( fixedSolution.GetDocument(sln.GetDocument(diagnostic.Location.SourceTree).Id), Formatter.Annotation, CancellationToken.None) .ConfigureAwait(false); CodeAssert.AreEqual(fixedCode, fixedSource); if (allowCompilationErrors == AllowCompilationErrors.No) { await VerifyNoCompilerErrorsAsync(fix, fixedSolution).ConfigureAwait(false); } }
private static async Task AreEqualAsync(IReadOnlyList <string> expected, Solution actual, string?messageHeader) { var actualCount = actual.Projects.SelectMany(x => x.Documents).Count(); if (expected.Count != actualCount) { throw new AssertException($"Expected {expected.Count} documents the fixed solution has {actualCount} documents."); } foreach (var project in actual.Projects) { foreach (var document in project.Documents) { var fixedSource = await CodeReader.GetStringFromDocumentAsync(document, CancellationToken.None).ConfigureAwait(false); CodeAssert.AreEqual(FindExpected(fixedSource), fixedSource, messageHeader); } } string FindExpected(string fixedSource) { var fixedNamespace = CodeReader.Namespace(fixedSource); var fixedFileName = CodeReader.FileName(fixedSource); var match = expected.FirstOrDefault(x => x == fixedSource); if (match != null) { return(match); } foreach (var candidate in expected) { if (CodeReader.Namespace(candidate) == fixedNamespace && CodeReader.FileName(candidate) == fixedFileName) { return(candidate); } } throw new AssertException($"The fixed solution contains a document {fixedFileName} in namespace {fixedNamespace} that is not in the expected documents."); } }
/// <summary> /// Create a new instance of <see cref="ExpectedDiagnostic"/> with position. /// </summary> /// <param name="codeWithErrorsIndicated">The code with error position indicated..</param> /// <param name="cleanedSources"><paramref name="codeWithErrorsIndicated"/> without errors indicated.</param> /// <returns>A new instance of <see cref="ExpectedDiagnostic"/>.</returns> public ExpectedDiagnostic WithPositionFromCodeWithErrorsIndicated(string codeWithErrorsIndicated, out string cleanedSources) { var positions = CodeReader.FindLinePositions(codeWithErrorsIndicated).ToArray(); if (positions.Length == 0) { throw new ArgumentException("Expected one error position indicated, was zero.", nameof(codeWithErrorsIndicated)); } if (positions.Length > 1) { throw new ArgumentException($"Expected one error position indicated, was {positions.Length}.", nameof(codeWithErrorsIndicated)); } cleanedSources = codeWithErrorsIndicated.Replace("↓", string.Empty); var fileName = CodeReader.FileName(codeWithErrorsIndicated); var position = positions[0]; return(new ExpectedDiagnostic(this.Id, this.Message, new FileLinePositionSpan(fileName, position, position))); }
/// <summary> /// Get the expected diagnostics and cleaned sources. /// </summary> /// <param name="analyzerId">The descriptor diagnosticId that is expected to produce diagnostics.</param> /// <param name="message">The expected message for the diagnostics, can be null.</param> /// <param name="code">The code with errors indicated.</param> /// <returns>An instance of <see cref="DiagnosticsAndSources"/>.</returns> public static DiagnosticsAndSources CreateFromCodeWithErrorsIndicated(string analyzerId, string?message, IReadOnlyList <string> code) { if (analyzerId is null) { throw new ArgumentNullException(nameof(analyzerId)); } if (code is null) { throw new ArgumentNullException(nameof(code)); } var diagnostics = new List <ExpectedDiagnostic>(); var cleanedSources = new List <string>(); foreach (var source in code) { var positions = CodeReader.FindLinePositions(source).ToArray(); if (positions.Length == 0) { cleanedSources.Add(source); continue; } cleanedSources.Add(source.Replace("↓", string.Empty)); var fileName = CodeReader.FileName(source); diagnostics.AddRange(positions.Select(p => new ExpectedDiagnostic(analyzerId, message, new FileLinePositionSpan(fileName, p, p)))); } if (diagnostics.Count == 0) { throw new InvalidOperationException("Expected code to have at least one error position indicated with '↓'"); } return(new DiagnosticsAndSources(diagnostics, cleanedSources)); }
public SourceMetadata(string code) { this.Code = code; this.FileName = CodeReader.FileName(code); this.Namespace = CodeReader.Namespace(code); }
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()); } }
/// <summary> /// Create a <see cref="Solution"/> for <paramref name="code"/> /// Each unique namespace in <paramref name="code"/> is added as a project. /// </summary> /// <param name="code">The code to create the solution from.</param> /// <param name="compilationOptions">The <see cref="CSharpCompilationOptions"/>.</param> /// <param name="metadataReferences">The metadata references.</param> /// <param name="languageVersion">The <see cref="LanguageVersion"/>.</param> /// <returns>A <see cref="Solution"/>.</returns> public static Solution CreateSolution(IEnumerable <string> code, CSharpCompilationOptions compilationOptions, IEnumerable <MetadataReference> metadataReferences = null, LanguageVersion languageVersion = LanguageVersion.Latest) { var solutionInfo = SolutionInfo.Create( SolutionId.CreateNewId("Test.sln"), VersionStamp.Default, projects: GetProjectInfos()); var solution = EmptySolution; foreach (var projectInfo in solutionInfo.Projects) { solution = solution.AddProject(projectInfo.WithProjectReferences(FindReferences(projectInfo))); } return(solution); IEnumerable <ProjectInfo> GetProjectInfos() { var byNamespace = new SortedDictionary <string, List <string> >(); foreach (var document in code) { var ns = CodeReader.Namespace(document); if (byNamespace.TryGetValue(ns, out var doc)) { doc.Add(document); } else { byNamespace[ns] = new List <string> { document }; } } var byProject = new SortedDictionary <string, List <KeyValuePair <string, List <string> > > >(); foreach (var kvp in byNamespace) { var last = byProject.Keys.LastOrDefault(); var ns = kvp.Key; if (last != null && ns.Contains(last)) { byProject[last].Add(kvp); } else { byProject.Add(ns, new List <KeyValuePair <string, List <string> > > { kvp }); } } foreach (var kvp in byProject) { var assemblyName = kvp.Key; var projectId = ProjectId.CreateNewId(assemblyName); yield return(ProjectInfo.Create( projectId, VersionStamp.Default, assemblyName, assemblyName, LanguageNames.CSharp, compilationOptions: compilationOptions, metadataReferences: metadataReferences, documents: kvp.Value.SelectMany(x => x.Value) .Select( x => { var documentName = CodeReader.FileName(x); return DocumentInfo.Create( DocumentId.CreateNewId(projectId, documentName), documentName, sourceCodeKind: SourceCodeKind.Regular, loader: new StringLoader(x)); })) .WithParseOptions(CSharpParseOptions.Default.WithLanguageVersion(languageVersion))); } } IEnumerable <ProjectReference> FindReferences(ProjectInfo projectInfo) { var references = new List <ProjectReference>(); foreach (var other in solutionInfo.Projects.Where(x => x.Id != projectInfo.Id)) { if (projectInfo.Documents.Any(x => x.TextLoader is StringLoader stringLoader && (stringLoader.Code.Contains($"using {other.Name};") || stringLoader.Code.Contains($"{other.Name}.")))) { references.Add(new ProjectReference(other.Id)); } } return(references); } }
/// <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) { if (IsAt(expected, expectedPos, "\\r") || IsAt(actual, actualPos, "\\r")) { if (IsAt(expected, expectedPos, "\\r")) { expectedPos += 2; } if (IsAt(actual, actualPos, "\\r")) { actualPos += 2; } continue; } var errorBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { errorBuilder.AppendLine(messageHeader); } if (!IsSingleLine(expected) || !IsSingleLine(actual)) { errorBuilder.AppendLine( CodeReader.TryGetFileName(expected, out var fileName) ? $"Mismatch on line {line} of file {fileName}." : $"Mismatch on line {line}."); } var expectedLine = expected.Split('\n')[line - 1].Trim('\r'); var actualLine = actual.Split('\n')[line - 1].Trim('\r'); var diffPos = DiffPos(expectedLine, actualLine); errorBuilder.AppendLine($"Expected: {expectedLine}") .AppendLine($"Actual: {actualLine}") .AppendLine($" {new string(' ', diffPos)}^"); if (!IsSingleLine(expected) || !IsSingleLine(actual)) { errorBuilder.AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine(); } throw new AssertException(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; } var messageBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { messageBuilder.AppendLine(messageHeader); } messageBuilder.AppendLine(CodeReader.TryGetFileName(expected, out var name) ? $"Mismatch at end of file {name}." : "Mismatch at end.") .Append("Expected: ").AppendLine(GetEnd(expected)) .Append("Actual: ").AppendLine(GetEnd(actual)) .AppendLine($" {new string(' ', DiffPos(GetEnd(expected), GetEnd(actual)))}^"); if (!IsSingleLine(expected) || !IsSingleLine(actual)) { messageBuilder.AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine(); } throw new AssertException(messageBuilder.Return()); bool IsSingleLine(string text) { bool foundNewLine = false; foreach (var c in text) { switch (c) { case '\r': case '\n': foundNewLine = true; break; default: if (foundNewLine) { return(false); } break; } } return(true); } int DiffPos(string expectedLine, string actualLine) { 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; } } return(diffPos); } string GetEnd(string text) { bool lastLine = false; var builder = StringBuilderPool.Borrow(); for (var i = text.Length - 1; i >= 0; i--) { switch (text[i]) { case '\r': if (lastLine) { return(builder.Return()); } builder.Insert(0, "\\r"); break; case '\n': if (lastLine) { return(builder.Return()); } builder.Insert(0, "\\n"); break; default: lastLine = true; builder.Insert(0, text[i]); break; } } return(builder.Return()); } }
private static async Task VerifyNoCompilerErrorsAsync(CodeFixProvider fix, 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) #pragma warning disable CS0618 // Suppress until removed. Will be replaced with Metadatareferences.FromAttributes() .Except(SuppressedDiagnostics) #pragma warning restore CS0618 // Suppress until removed. Will be replaced with Metadatareferences.FromAttributes() .Any()) { var errorBuilder = StringBuilderPool.Borrow(); errorBuilder.AppendLine($"{fix.GetType().Name} introduced syntax error{(introducedDiagnostics.Length > 1 ? "s" : string.Empty)}."); foreach (var introducedDiagnostic in introducedDiagnostics) { errorBuilder.AppendLine($"{introducedDiagnostic.ToErrorString()}"); } var sources = await Task.WhenAll(fixedSolution.Projects.SelectMany(p => p.Documents).Select(d => CodeReader.GetStringFromDocumentAsync(d, CancellationToken.None))); errorBuilder.AppendLine("First source file with error is:"); var lineSpan = introducedDiagnostics.First().Location.GetMappedLineSpan(); if (sources.TrySingle(x => CodeReader.FileName(x) == lineSpan.Path, out var match)) { errorBuilder.AppendLine(match); } else if (sources.TryFirst(x => CodeReader.FileName(x) == lineSpan.Path, out _)) { errorBuilder.AppendLine($"Found more than one document for {lineSpan.Path}."); foreach (string source in sources.Where(x => CodeReader.FileName(x) == lineSpan.Path)) { errorBuilder.AppendLine(source); } } else { errorBuilder.AppendLine($"Did not find a single document for {lineSpan.Path}."); } throw new AssertException(errorBuilder.Return()); } }
/// <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); } }
/// <summary> /// Verifies that /// 1. <paramref name="solution"/> produces the expected diagnostics /// 2. The code fix fixes the code. /// </summary> /// <param name="analyzer">The analyzer to run on the code..</param> /// <param name="fix">The code fix to apply.</param> /// <param name="expectedDiagnostic">The expected diagnostic.</param> /// <param name="solution">The code with error positions indicated.</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="allowCompilationErrors">If compilation errors are accepted in the fixed code.</param> public static void CodeFix(DiagnosticAnalyzer analyzer, CodeFixProvider fix, ExpectedDiagnostic expectedDiagnostic, Solution solution, string fixedCode, string fixTitle = null, AllowCompilationErrors allowCompilationErrors = AllowCompilationErrors.No) { VerifyAnalyzerSupportsDiagnostic(analyzer, expectedDiagnostic); VerifyCodeFixSupportsAnalyzer(analyzer, fix); var diagnostics = Analyze.GetDiagnostics(analyzer, solution); var diagnosticsAndSources = DiagnosticsAndSources.Create(expectedDiagnostic, solution.Projects.SelectMany(x => x.Documents).Select(x => CodeReader.GetCode(x, null)).ToArray()); VerifyDiagnostics(diagnosticsAndSources, diagnostics); VerifyFix(solution, diagnostics, analyzer, fix, fixedCode, fixTitle, allowCompilationErrors); }
/// <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) { if (IsAt(expected, expectedPos, "\\r") || IsAt(actual, actualPos, "\\r")) { if (IsAt(expected, expectedPos, "\\r")) { expectedPos += 2; } if (IsAt(actual, actualPos, "\\r")) { actualPos += 2; } continue; } var errorBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { errorBuilder.AppendLine(messageHeader); } if (!IsSingleLine(expected) || !IsSingleLine(actual)) { errorBuilder.AppendLine( CodeReader.TryGetFileName(expected, out var fileName) ? $"Mismatch on line {line} of file {fileName}." : $"Mismatch on line {line}."); } var expectedLine = expected.Split('\n')[line - 1].Trim('\r'); var actualLine = actual.Split('\n')[line - 1].Trim('\r'); var diffPos = DiffPos(expectedLine, actualLine); errorBuilder.AppendLine($"Expected: {expectedLine}") .AppendLine($"Actual: {actualLine}") .AppendLine($" {new string(' ', diffPos)}^"); if (!IsSingleLine(expected) || !IsSingleLine(actual)) { errorBuilder.AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine(); } throw new AssertException(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; } var messageBuilder = StringBuilderPool.Borrow(); if (messageHeader != null) { messageBuilder.AppendLine(messageHeader); } messageBuilder.AppendLine(CodeReader.TryGetFileName(expected, out var name) ? $"Mismatch at end of file {name}." : "Mismatch at end.") .Append("Expected: ").AppendLine(GetEnd(expected)) .Append("Actual: ").AppendLine(GetEnd(actual)) .AppendLine($" {new string(' ', DiffPos(GetEnd(expected), GetEnd(actual)))}^"); if (!IsSingleLine(expected) || !IsSingleLine(actual)) { messageBuilder.AppendLine("Expected:") .Append(expected) .AppendLine() .AppendLine("Actual:") .Append(actual) .AppendLine(); } throw new AssertException(messageBuilder.Return());