diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs index 5c749ede2..7fa66d379 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs @@ -12,6 +12,7 @@ namespace TestHelper using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Simplification; + using Microsoft.CodeAnalysis.Text; /// /// Diagnostic Producer class with extra methods dealing with applying code fixes. @@ -23,16 +24,16 @@ public abstract partial class CodeFixVerifier : DiagnosticVerifier /// Apply the inputted to the inputted document. /// Meant to be used to apply code fixes. /// - /// The to apply the fix on + /// The to apply the fix on /// A that will be applied to the - /// . + /// . /// The that the task will observe. - /// A with the changes from the . - private static async Task ApplyFixAsync(Document document, CodeAction codeAction, CancellationToken cancellationToken) + /// A with the changes from the . + private static async Task ApplyFixAsync(Project project, CodeAction codeAction, CancellationToken cancellationToken) { var operations = await codeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false); var solution = operations.OfType().Single().ChangedSolution; - return solution.GetDocument(document.Id); + return solution.GetProject(project.Id); } /// @@ -75,13 +76,20 @@ private static IEnumerable GetNewDiagnostics(IEnumerable /// /// Get the existing compiler diagnostics on the input document. /// - /// The to run the compiler diagnostic analyzers on. + /// The to run the compiler diagnostic analyzers on. /// The that the task will observe. /// The compiler diagnostics that were found in the code. - private static async Task> GetCompilerDiagnosticsAsync(Document document, CancellationToken cancellationToken) + private static async Task> GetCompilerDiagnosticsAsync(Project project, CancellationToken cancellationToken) { - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - return semanticModel.GetDiagnostics(cancellationToken: cancellationToken); + var allDiagnostics = ImmutableArray.Create(); + + foreach (var document in project.Documents) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + allDiagnostics = allDiagnostics.AddRange(semanticModel.GetDiagnostics(cancellationToken: cancellationToken)); + } + + return allDiagnostics; } /// @@ -97,5 +105,49 @@ private static async Task GetStringFromDocumentAsync(Document document, var sourceText = await formatted.GetTextAsync(cancellationToken).ConfigureAwait(false); return sourceText.ToString(); } + + /// + /// Implements a workaround for issue #936, force re-parsing to get the same sort of syntax tree as the original document. + /// + /// The project to update. + /// The . + /// The updated . + private static async Task RecreateProjectDocumentsAsync(Project project, CancellationToken cancellationToken) + { + foreach (var documentId in project.DocumentIds) + { + var document = project.GetDocument(documentId); + document = await RecreateDocumentAsync(document, cancellationToken).ConfigureAwait(false); + project = document.Project; + } + + return project; + } + + private static async Task RecreateDocumentAsync(Document document, CancellationToken cancellationToken) + { + var newText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + newText = newText.WithChanges(new TextChange(new TextSpan(0, 0), " ")); + newText = newText.WithChanges(new TextChange(new TextSpan(0, 1), string.Empty)); + return document.WithText(newText); + } + + /// + /// Formats the whitespace in all documents of the specified . + /// + /// The project to update. + /// The . + /// The updated . + private static async Task ReformatProjectDocumentsAsync(Project project, CancellationToken cancellationToken) + { + foreach (var documentId in project.DocumentIds) + { + var document = project.GetDocument(documentId); + document = await Formatter.FormatAsync(document, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); + project = document.Project; + } + + return project; + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs index 280aa1d21..80330e301 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/FileMayOnlyContainTestBase.cs @@ -3,6 +3,7 @@ namespace StyleCop.Analyzers.Test.MaintainabilityRules { + using System.Linq; using System.Threading; using System.Threading.Tasks; using TestHelper; @@ -20,7 +21,10 @@ public async Task TestOneElementAsync() var testCode = @"%1 Foo { }"; - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + + testCode = testCode.Replace("%1", this.Keyword); + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } [Fact] @@ -32,18 +36,29 @@ public async Task TestTwoElementsAsync() %1 Bar { }"; - var fixedCode = @"%1 Foo + + var fixedCode = new[] + { + @"%1 Foo { } -"; +", + @"%1 Bar +{ +}" + }; + + testCode = testCode.Replace("%1", this.Keyword); + fixedCode = fixedCode.Select(c => c.Replace("%1", this.Keyword)).ToArray(); DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2); - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } } @@ -59,22 +74,37 @@ public async Task TestThreeElementsAsync() %1 FooBar { }"; - var fixedCode = @"%1 Foo + + var fixedCode = new[] + { + @"%1 Foo +{ +} +", + @"%1 Bar { } -"; +", + @"%1 FooBar +{ +}" + }; + + testCode = testCode.Replace("%1", this.Keyword); + fixedCode = fixedCode.Select(code => code.Replace("%1", this.Keyword)).ToArray(); DiagnosticResult[] expected = - { - this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2), - this.CSharpDiagnostic().WithLocation(7, this.Keyword.Length + 2) - }; + { + this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2), + this.CSharpDiagnostic().WithLocation(7, this.Keyword.Length + 2) + }; + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } } @@ -89,18 +119,31 @@ public async Task TestRemoveWarningSuppressionAsync() #pragma warning disable SomeWarning #pragma warning restore SomeWarning }"; - var fixedCode = @"%1 Foo + + var fixedCode = new[] + { + @"%1 Foo { } -"; +", + @"%1 Bar +{ +#pragma warning disable SomeWarning +#pragma warning restore SomeWarning +}" + }; + + testCode = testCode.Replace("%1", this.Keyword); + fixedCode = fixedCode.Select(code => code.Replace("%1", this.Keyword)).ToArray(); DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2); - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } } @@ -116,18 +159,30 @@ public async Task TestPreserveWarningSuppressionAsync() }"; // See https://github.com/dotnet/roslyn/issues/3999 - var fixedCode = @"%1 Foo + var fixedCode = new[] + { + @"%1 Foo { } -"; +", + @"%1 Bar +{ +#pragma warning disable SomeWarning +}" + }; DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2); await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + + foreach (var code in fixedCode) + { + await this.VerifyCSharpDiagnosticAsync(code.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode.Replace("%1", this.Keyword) }, fixedCode.Select(c => c.Replace("%1", this.Keyword)).ToArray(), cancellationToken: CancellationToken.None).ConfigureAwait(false); } } @@ -142,18 +197,31 @@ public async Task TestRemovePreprocessorDirectivesAsync() #if true #endif }"; - var fixedCode = @"%1 Foo + + var fixedCode = new[] + { + @"%1 Foo { } -"; +", + @"%1 Bar +{ +#if true +#endif +}" + }; + + testCode = testCode.Replace("%1", this.Keyword); + fixedCode = fixedCode.Select(code => code.Replace("%1", this.Keyword)).ToArray(); DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, this.Keyword.Length + 2); - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } } @@ -170,21 +238,34 @@ public async Task TestPreservePreprocessorDirectivesAsync() }"; // See https://github.com/dotnet/roslyn/issues/3999 - var fixedCode = @"%1 Foo + var fixedCode = new[] + { + @"%1 Foo { #if true } #endif -"; +", + @" +#if true +%1 Bar +{ +#endif +}" + }; + + testCode = testCode.Replace("%1", this.Keyword); + fixedCode = fixedCode.Select(code => code.Replace("%1", this.Keyword)).ToArray(); DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(5, this.Keyword.Length + 2); - await this.VerifyCSharpDiagnosticAsync(testCode.Replace("%1", this.Keyword), expected, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpDiagnosticAsync(fixedCode.Replace("%1", this.Keyword), EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + if (this.SupportsCodeFix) { - await this.VerifyCSharpFixAsync(testCode.Replace("%1", this.Keyword), fixedCode.Replace("%1", this.Keyword), cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/SA1402UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/SA1402UnitTests.cs index 639d976af..5b9b6bcaa 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/SA1402UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/MaintainabilityRules/SA1402UnitTests.cs @@ -48,16 +48,24 @@ public partial class Bar { }"; - var fixedCode = @"public partial class Foo + + var fixedCode = new[] + { + @"public partial class Foo { } -"; +", + @"public partial class Bar +{ + +}" + }; DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(4, 22); await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } [Fact] @@ -70,15 +78,22 @@ public class Test0 { }"; - var fixedCode = @"public class Test0 + var fixedCode = new[] + { + @"public class Test0 { -}"; +}", + @"public class Foo +{ +} +" + }; DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(1, 14); await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); - await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(new[] { testCode }, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); } [Fact] diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/CodeFixVerifier.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/CodeFixVerifier.cs index 71ff0e455..942b459a4 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/CodeFixVerifier.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/Verifiers/CodeFixVerifier.cs @@ -8,6 +8,7 @@ namespace TestHelper using System.Collections.Immutable; using System.Diagnostics; using System.Linq; + using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -24,6 +25,8 @@ namespace TestHelper /// public abstract partial class CodeFixVerifier : DiagnosticVerifier { + private const int DefaultNumberOfIncrementalIterations = -1000; + /// /// Returns the code fix being tested (C#) - to be implemented in non-abstract class. /// @@ -45,9 +48,29 @@ public abstract partial class CodeFixVerifier : DiagnosticVerifier /// value is less than 0, the negated value is treated as an upper limit as opposed to an exact value. /// The that the task will observe. /// A representing the asynchronous operation. - protected async Task VerifyCSharpFixAsync(string oldSource, string newSource, string batchNewSource = null, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIncrementalIterations = -int.MaxValue, int numberOfFixAllIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) + protected Task VerifyCSharpFixAsync(string oldSource, string newSource, string batchNewSource = null, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIncrementalIterations = DefaultNumberOfIncrementalIterations, int numberOfFixAllIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) { - var t1 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics, numberOfIncrementalIterations, GetSingleAnalyzerDocumentAsync, cancellationToken).ConfigureAwait(false); + return this.VerifyCSharpFixAsync(new[] { oldSource }, new[] { newSource }, batchNewSource == null ? null : new[] { batchNewSource }, codeFixIndex, allowNewCompilerDiagnostics, numberOfIncrementalIterations, numberOfFixAllIterations, cancellationToken); + } + + /// + /// Called to test a C# code fix when applied on the input source as a string. + /// + /// An array of sources in the form of strings before the code fix was applied to them. + /// An array of sources in the form of strings after the code fix was applied to them. + /// An array of sources in the form of a strings after the batch fixer was applied to them. + /// Index determining which code fix to apply if there are multiple. + /// A value indicating whether or not the test will fail if the code fix introduces other warnings after being applied. + /// The number of iterations the incremental fixer will be called. + /// If this value is less than 0, the negated value is treated as an upper limit as opposed to an exact + /// value. + /// The number of iterations the Fix All fixer will be called. If this + /// value is less than 0, the negated value is treated as an upper limit as opposed to an exact value. + /// The that the task will observe. + /// A representing the asynchronous operation. + protected async Task VerifyCSharpFixAsync(string[] oldSources, string[] newSources, string[] batchNewSources = null, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIncrementalIterations = DefaultNumberOfIncrementalIterations, int numberOfFixAllIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + var t1 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, newSources, codeFixIndex, allowNewCompilerDiagnostics, numberOfIncrementalIterations, FixEachAnalyzerDiagnosticAsync, cancellationToken).ConfigureAwait(false); var fixAllProvider = this.GetCSharpCodeFixProvider().GetFixAllProvider(); Assert.NotEqual(WellKnownFixAllProviders.BatchFixer, fixAllProvider); @@ -63,19 +86,19 @@ public abstract partial class CodeFixVerifier : DiagnosticVerifier await t1; } - var t2 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, batchNewSource ?? newSource, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, GetFixAllAnalyzerDocumentAsync, cancellationToken).ConfigureAwait(false); + var t2 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInDocumentAsync, cancellationToken).ConfigureAwait(false); if (Debugger.IsAttached) { await t2; } - var t3 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, batchNewSource ?? newSource, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, GetFixAllAnalyzerProjectAsync, cancellationToken).ConfigureAwait(false); + var t3 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInProjectAsync, cancellationToken).ConfigureAwait(false); if (Debugger.IsAttached) { await t3; } - var t4 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, batchNewSource ?? newSource, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, GetFixAllAnalyzerSolutionAsync, cancellationToken).ConfigureAwait(false); + var t4 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInSolutionAsync, cancellationToken).ConfigureAwait(false); if (Debugger.IsAttached) { await t4; @@ -105,7 +128,7 @@ public abstract partial class CodeFixVerifier : DiagnosticVerifier /// A representing the asynchronous operation. protected async Task VerifyCSharpFixAllFixAsync(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) { - await this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics, numberOfIterations, GetFixAllAnalyzerDocumentAsync, cancellationToken).ConfigureAwait(false); + await this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), new[] { oldSource }, new[] { newSource }, codeFixIndex, allowNewCompilerDiagnostics, numberOfIterations, FixAllAnalyzerDiagnosticsInDocumentAsync, cancellationToken).ConfigureAwait(false); } /// @@ -120,7 +143,7 @@ public abstract partial class CodeFixVerifier : DiagnosticVerifier return await this.GetOfferedFixesInternalAsync(LanguageNames.CSharp, source, diagnosticIndex, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), cancellationToken).ConfigureAwait(false); } - private static async Task GetSingleAnalyzerDocumentAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Document document, int numberOfIterations, CancellationToken cancellationToken) + private static async Task FixEachAnalyzerDiagnosticAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) { int expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) @@ -133,7 +156,7 @@ private static async Task GetSingleAnalyzerDocumentAsync(ImmutableArra bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, new[] { document }, cancellationToken).ConfigureAwait(false); + var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, project.Documents.ToArray(), cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -152,28 +175,26 @@ private static async Task GetSingleAnalyzerDocumentAsync(ImmutableArra previousDiagnostics = analyzerDiagnostics; done = true; - for (var i = 0; i < analyzerDiagnostics.Length; i++) + foreach (var diagnostic in analyzerDiagnostics) { - if (!codeFixProvider.FixableDiagnosticIds.Contains(analyzerDiagnostics[i].Id)) + if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) { // do not pass unsupported diagnostics to a code fix provider continue; } var actions = new List(); - var context = new CodeFixContext(document, analyzerDiagnostics[i], (a, d) => actions.Add(a), cancellationToken); + var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); if (actions.Count > 0) { - var fixedDocument = await ApplyFixAsync(document, actions.ElementAt(codeFixIndex.GetValueOrDefault(0)), cancellationToken).ConfigureAwait(false); - if (fixedDocument != document) + var fixedProject = await ApplyFixAsync(project, actions.ElementAt(codeFixIndex.GetValueOrDefault(0)), cancellationToken).ConfigureAwait(false); + if (fixedProject != project) { done = false; - var newText = await fixedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - // workaround for issue #936 - force re-parsing to get the same sort of syntax tree as the original document. - document = document.WithText(newText); + project = await RecreateProjectDocumentsAsync(fixedProject, cancellationToken).ConfigureAwait(false); break; } } @@ -186,25 +207,25 @@ private static async Task GetSingleAnalyzerDocumentAsync(ImmutableArra Assert.Equal($"{expectedNumberOfIterations} iterations", $"{expectedNumberOfIterations - numberOfIterations} iterations"); } - return document; + return project; } - private static Task GetFixAllAnalyzerDocumentAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Document document, int numberOfIterations, CancellationToken cancellationToken) + private static Task FixAllAnalyzerDiagnosticsInDocumentAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) { - return GetFixAllAnalyzerAsync(FixAllScope.Document, analyzers, codeFixProvider, codeFixIndex, document, numberOfIterations, cancellationToken); + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Document, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); } - private static Task GetFixAllAnalyzerProjectAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Document document, int numberOfIterations, CancellationToken cancellationToken) + private static Task FixAllAnalyzerDiagnosticsInProjectAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) { - return GetFixAllAnalyzerAsync(FixAllScope.Project, analyzers, codeFixProvider, codeFixIndex, document, numberOfIterations, cancellationToken); + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Project, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); } - private static Task GetFixAllAnalyzerSolutionAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Document document, int numberOfIterations, CancellationToken cancellationToken) + private static Task FixAllAnalyzerDiagnosticsInSolutionAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) { - return GetFixAllAnalyzerAsync(FixAllScope.Solution, analyzers, codeFixProvider, codeFixIndex, document, numberOfIterations, cancellationToken); + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Solution, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); } - private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Document document, int numberOfIterations, CancellationToken cancellationToken) + private static async Task FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope scope, ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) { int expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) @@ -224,7 +245,7 @@ private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, Im bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, new[] { document }, cancellationToken).ConfigureAwait(false); + var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, project.Documents.ToArray(), cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -240,6 +261,7 @@ private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, Im Assert.True(false, "The upper limit for the number of fix all iterations was exceeded"); } + Diagnostic firstDiagnostic = null; string equivalenceKey = null; foreach (var diagnostic in analyzerDiagnostics) { @@ -250,15 +272,21 @@ private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, Im } var actions = new List(); - var context = new CodeFixContext(document, diagnostic, (a, d) => actions.Add(a), cancellationToken); + var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); if (actions.Count > (codeFixIndex ?? 0)) { + firstDiagnostic = diagnostic; equivalenceKey = actions[codeFixIndex ?? 0].EquivalenceKey; break; } } + if (firstDiagnostic == null) + { + return project; + } + previousDiagnostics = analyzerDiagnostics; done = true; @@ -267,24 +295,22 @@ private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, Im IEnumerable analyzerDiagnosticIds = analyzers.SelectMany(x => x.SupportedDiagnostics).Select(x => x.Id); IEnumerable compilerDiagnosticIds = codeFixProvider.FixableDiagnosticIds.Where(x => x.StartsWith("CS", StringComparison.Ordinal)); - IEnumerable disabledDiagnosticIds = document.Project.CompilationOptions.SpecificDiagnosticOptions.Where(x => x.Value == ReportDiagnostic.Suppress).Select(x => x.Key); + IEnumerable disabledDiagnosticIds = project.CompilationOptions.SpecificDiagnosticOptions.Where(x => x.Value == ReportDiagnostic.Suppress).Select(x => x.Key); IEnumerable relevantIds = analyzerDiagnosticIds.Concat(compilerDiagnosticIds).Except(disabledDiagnosticIds).Distinct(); - FixAllContext fixAllContext = new FixAllContext(document, codeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); + FixAllContext fixAllContext = new FixAllContext(project.GetDocument(firstDiagnostic.Location.SourceTree), codeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); CodeAction action = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false); if (action == null) { - return document; + return project; } - var fixedDocument = await ApplyFixAsync(document, action, cancellationToken).ConfigureAwait(false); - if (fixedDocument != document) + var fixedProject = await ApplyFixAsync(project, action, cancellationToken).ConfigureAwait(false); + if (fixedProject != project) { done = false; - var newText = await fixedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - // workaround for issue #936 - force re-parsing to get the same sort of syntax tree as the original document. - document = document.WithText(newText); + project = await RecreateProjectDocumentsAsync(fixedProject, cancellationToken).ConfigureAwait(false); } } while (!done); @@ -294,7 +320,7 @@ private static async Task GetFixAllAnalyzerAsync(FixAllScope scope, Im Assert.Equal($"{expectedNumberOfIterations} iterations", $"{expectedNumberOfIterations - numberOfIterations} iterations"); } - return document; + return project; } private static bool AreDiagnosticsDifferent(ImmutableArray analyzerDiagnostics, ImmutableArray previousDiagnostics) @@ -320,39 +346,51 @@ private async Task VerifyFixInternalAsync( string language, ImmutableArray analyzers, CodeFixProvider codeFixProvider, - string oldSource, - string newSource, + string[] oldSources, + string[] newSources, int? codeFixIndex, bool allowNewCompilerDiagnostics, int numberOfIterations, - Func, CodeFixProvider, int?, Document, int, CancellationToken, Task> getFixedDocument, + Func, CodeFixProvider, int?, Project, int, CancellationToken, Task> getFixedProject, CancellationToken cancellationToken) { - var document = this.CreateDocument(oldSource, language); - var compilerDiagnostics = await GetCompilerDiagnosticsAsync(document, cancellationToken).ConfigureAwait(false); + var project = this.CreateProject(oldSources, language); + var compilerDiagnostics = await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false); - document = await getFixedDocument(analyzers, codeFixProvider, codeFixIndex, document, numberOfIterations, cancellationToken).ConfigureAwait(false); + project = await getFixedProject(analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken).ConfigureAwait(false); - var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(document, cancellationToken).ConfigureAwait(false)); + var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false)); - // check if applying the code fix introduced any new compiler diagnostics + // Check if applying the code fix introduced any new compiler diagnostics if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) { // Format and get the compiler diagnostics again so that the locations make sense in the output - document = await Formatter.FormatAsync(document, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); - newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(document, cancellationToken).ConfigureAwait(false)); - - string message = - string.Format( - "Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", - string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), - (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)).ToFullString()); - Assert.True(false, message); + project = await ReformatProjectDocumentsAsync(project, cancellationToken).ConfigureAwait(false); + newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false)); + + var message = new StringBuilder(); + message.Append("Fix introduced new compiler diagnostics:\r\n"); + newCompilerDiagnostics.Aggregate(message, (sb, d) => sb.Append(d.ToString()).Append("\r\n")); + foreach (var document in project.Documents) + { + message.Append("\r\n").Append(document.Name).Append(":\r\n"); + message.Append((await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)).ToFullString()); + message.Append("\r\n"); + } + + Assert.True(false, message.ToString()); } - // after applying all of the code fixes, compare the resulting string to the inputted one - var actual = await GetStringFromDocumentAsync(document, cancellationToken).ConfigureAwait(false); - Assert.Equal(newSource, actual); + // After applying all of the code fixes, compare the resulting string to the inputted one + var updatedDocuments = project.Documents.ToArray(); + + Assert.Equal($"{newSources.Length} documents", $"{updatedDocuments.Length} documents"); + + for (int i = 0; i < updatedDocuments.Length; i++) + { + var actual = await GetStringFromDocumentAsync(updatedDocuments[i], cancellationToken).ConfigureAwait(false); + Assert.Equal(newSources[i], actual); + } } private async Task> GetOfferedFixesInternalAsync(string language, string source, int? diagnosticIndex, ImmutableArray analyzers, CodeFixProvider codeFixProvider, CancellationToken cancellationToken)