From 876f52e9ff9491c5f0191927b1617c18d2cc3809 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 21 Jun 2021 09:46:26 -0700 Subject: [PATCH 1/3] Fix support for multiple languages in AdditionalProjects --- .../ProjectCollection.cs | 17 +- .../MultipleProjectsTests.cs | 257 ++++++++++++++++++ 2 files changed, 273 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs index 07ee2603d..12b69c14e 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs @@ -35,7 +35,22 @@ public ProjectCollection(string defaultLanguage, string defaultExtension) { get { - var project = this.GetOrAdd(projectName, () => new ProjectState(projectName, _defaultLanguage, $"/{projectName}/Test", _defaultExtension)); + string extension; + if (language == _defaultLanguage) + { + extension = _defaultExtension; + } + else + { + extension = language switch + { + LanguageNames.CSharp => "cs", + LanguageNames.VisualBasic => "vb", + _ => throw new ArgumentOutOfRangeException(nameof(language)), + }; + } + + var project = this.GetOrAdd(projectName, () => new ProjectState(projectName, language, $"/{projectName}/Test", extension)); if (project.Language != language) { throw new InvalidOperationException(); diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs index 0ecfcd1e9..118ba2b01 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs @@ -12,6 +12,8 @@ Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer, Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpAnalyzerTest, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using VisualBasicTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.VisualBasicAnalyzerTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer>; namespace Microsoft.CodeAnalysis.Testing { @@ -44,6 +46,87 @@ public async Task TwoCSharpProjects_Independent() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Derived1 : {|CS0246:Base2|} [|{|] }", + @"public class Base1 [|{|] }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Derived2 : {|CS0246:Base1|} [|{|] }", + @"public class Base2 [|{|] }", + }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_IndependentWithMarkupLocations() { @@ -120,6 +203,93 @@ public async Task TwoCSharpProjects_PrimaryReferencesSecondary() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits Base2 : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 [|{|] object field = new Type3(); }", + @"public class Type2 [|{|] }", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field As Object = New {|BC30002:Type1|}() : End Class", + @"Public Class Type4 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field As Object = New Type3() : End Class", + @"Public Class Type2 : End Class", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 [|{|] object field = new {|CS0246:Type1|}(); }", + @"public class Type4 [|{|] }", + }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_SecondaryReferencesPrimary() { @@ -149,6 +319,93 @@ public async Task TwoCSharpProjects_SecondaryReferencesPrimary() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits Base1 : End Class", + @"Public Class Base2 : End Class", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 [|{|] object field = new {|CS0246:Type3|}(); }", + @"public class Type2 [|{|] }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field As Object = New Type1() : End Class", + @"Public Class Type4 : End Class", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field As Object = New {|BC30002:Type3|}() : End Class", + @"Public Class Type2 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 [|{|] object field = new Type1(); }", + @"public class Type4 [|{|] }", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_DefaultPaths() { From 815b3772cb0ea732c1b34cf5a813619b942f473a Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 21 Jun 2021 16:31:23 -0700 Subject: [PATCH 2/3] Improve triggering and validation for multi-project scenarios Fixes #806 --- eng/Versions.props | 1 + .../CodeActionTest`1.cs | 38 ++- ...osoft.CodeAnalysis.Analyzer.Testing.csproj | 1 + .../PublicAPI.Unshipped.txt | 2 +- .../CodeFixTest`1.cs | 135 ++++++-- .../PublicAPI.Unshipped.txt | 2 + .../CodeRefactoringTest`1.cs | 50 ++- .../CodeFixIterationTests.cs | 81 +---- .../FixMultipleProjectsTests.cs | 306 ++++++++++++++++++ .../RefactoringValidationTests.cs | 87 +++++ ...soft.CodeAnalysis.Testing.Utilities.csproj | 1 + .../TestAnalyzers/LiteralUnderFiveAnalyzer.cs | 41 +++ .../TestFixes/IncrementFix.cs | 65 ++++ 13 files changed, 698 insertions(+), 112 deletions(-) create mode 100644 tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs create mode 100644 tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs create mode 100644 tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs diff --git a/eng/Versions.props b/eng/Versions.props index a0188b0fa..bdaa950b9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -74,6 +74,7 @@ 2.6.1 3.9.0 1.0.1-beta1.20374.2 + $(xunitVersion) 1.2.7 0.1.49-beta diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs index 459395457..73f93a627 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs @@ -70,12 +70,44 @@ protected static bool CodeActionExpected(SolutionState state) || state.AdditionalFilesFactories.Any(); } - protected static bool HasAnyChange(SolutionState oldState, SolutionState newState) + protected static bool HasAnyChange(ProjectState oldState, ProjectState newState, bool recursive) { - return !oldState.Sources.SequenceEqual(newState.Sources, SourceFileEqualityComparer.Instance) + if (!oldState.Sources.SequenceEqual(newState.Sources, SourceFileEqualityComparer.Instance) || !oldState.GeneratedSources.SequenceEqual(newState.GeneratedSources, SourceFileEqualityComparer.Instance) || !oldState.AdditionalFiles.SequenceEqual(newState.AdditionalFiles, SourceFileEqualityComparer.Instance) - || !oldState.AnalyzerConfigFiles.SequenceEqual(newState.AnalyzerConfigFiles, SourceFileEqualityComparer.Instance); + || !oldState.AnalyzerConfigFiles.SequenceEqual(newState.AnalyzerConfigFiles, SourceFileEqualityComparer.Instance)) + { + return true; + } + + if (!recursive) + { + return false; + } + + if (oldState is SolutionState oldSolutionState) + { + if (!(newState is SolutionState newSolutionState)) + { + throw new ArgumentException("Unexpected mismatch of SolutionState with ProjectState."); + } + + if (oldSolutionState.AdditionalProjects.Count != newSolutionState.AdditionalProjects.Count) + { + return true; + } + + foreach (var oldAdditionalState in oldSolutionState.AdditionalProjects) + { + if (!newSolutionState.AdditionalProjects.TryGetValue(oldAdditionalState.Key, out var newAdditionalState) + || HasAnyChange(oldAdditionalState.Value, newAdditionalState, recursive: true)) + { + return true; + } + } + } + + return false; } protected static CodeAction? TryGetCodeActionToApply(ImmutableArray actions, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, IVerifier verifier) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj index d108fdc13..91f8237e9 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt index 2ab84d52f..14d5b1ccb 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt @@ -245,7 +245,7 @@ static Microsoft.CodeAnalysis.Testing.AnalyzerVerifier.Diagnostic(string diagnosticId) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult static Microsoft.CodeAnalysis.Testing.AnalyzerVerifier.VerifyAnalyzerAsync(string source, params Microsoft.CodeAnalysis.Testing.DiagnosticResult[] expected) -> System.Threading.Tasks.Task static Microsoft.CodeAnalysis.Testing.CodeActionTest.CodeActionExpected(Microsoft.CodeAnalysis.Testing.SolutionState state) -> bool -static Microsoft.CodeAnalysis.Testing.CodeActionTest.HasAnyChange(Microsoft.CodeAnalysis.Testing.SolutionState oldState, Microsoft.CodeAnalysis.Testing.SolutionState newState) -> bool +static Microsoft.CodeAnalysis.Testing.CodeActionTest.HasAnyChange(Microsoft.CodeAnalysis.Testing.ProjectState oldState, Microsoft.CodeAnalysis.Testing.ProjectState newState, bool recursive) -> bool static Microsoft.CodeAnalysis.Testing.CodeActionTest.TryGetCodeActionToApply(System.Collections.Immutable.ImmutableArray actions, int? codeActionIndex, string codeActionEquivalenceKey, System.Action codeActionVerifier, Microsoft.CodeAnalysis.Testing.IVerifier verifier) -> Microsoft.CodeAnalysis.CodeActions.CodeAction static Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerError(string identifier) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult static Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerWarning(string identifier) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs index c207545c8..de2c8c122 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs @@ -122,7 +122,8 @@ public string BatchFixedCode /// /// /// If the expected Fix All output equals the input sources, the default value is treated as 0. - /// Otherwise, the default value is treated as 1. + /// If all projects in the solution have the same , the default value is treated as 1. + /// Otherwise, the default value is treated as new negative of the number of languages represented by projects in the solution. /// /// /// @@ -158,6 +159,31 @@ public string BatchFixedCode /// public int? NumberOfFixAllInDocumentIterations { get; set; } + /// + /// Gets or sets the number of code fix iterations expected during code fix testing for Fix All in Project + /// scenarios. + /// + /// + /// See the property for an overview of the behavior of this + /// property. If the number of Fix All in Project iterations is not specified, the value is automatically + /// selected according to the current test configuration: + /// + /// + /// If a value has been explicitly provided for , the value is used as-is. + /// If the expected Fix All output equals the input sources, the default value is treated as 0. + /// Otherwise, the default value is treated as the negative of the number of distinct projects containing fixable diagnostics (typically -1). + /// + /// + /// + /// The default value for this property can be interpreted as "Fix All in Project operations are expected + /// to complete after at most one operation for each fixable project in the input source has been applied. + /// Completing in fewer iterations is acceptable." + /// + /// + /// + /// + public int? NumberOfFixAllInProjectIterations { get; set; } + /// /// Gets or sets the code fix test behaviors applying to this test. The default value is /// . @@ -266,8 +292,12 @@ private bool CodeFixExpected() /// A representing the asynchronous operation. protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixedState, SolutionState batchFixedState, IVerifier verifier, CancellationToken cancellationToken) { + var fixers = GetCodeFixProviders().ToImmutableArray(); + var fixableDiagnostics = testState.ExpectedDiagnostics.Where(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))).ToImmutableArray(); + int numberOfIncrementalIterations; int numberOfFixAllIterations; + int numberOfFixAllInProjectIterations; int numberOfFixAllInDocumentIterations; if (NumberOfIncrementalIterations != null) { @@ -275,16 +305,14 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, fixedState)) + if (!HasAnyChange(testState, fixedState, recursive: true)) { numberOfIncrementalIterations = 0; } else { // Expect at most one iteration per fixable diagnostic - var fixers = GetCodeFixProviders().ToArray(); - var fixableExpectedDiagnostics = testState.ExpectedDiagnostics.Count(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))); - numberOfIncrementalIterations = -fixableExpectedDiagnostics; + numberOfIncrementalIterations = -fixableDiagnostics.Count(); } } @@ -294,13 +322,44 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, batchFixedState)) + if (!HasAnyChange(testState, batchFixedState, recursive: true)) { numberOfFixAllIterations = 0; } else { - numberOfFixAllIterations = 1; + // Expect at most one iteration per language with fixable diagnostics. Since we can't tell the + // language from ExpectedDiagnostic, use a conservative value from the number of project languages + // present. + numberOfFixAllIterations = -Enumerable.Repeat(testState.Language, 1).Concat(testState.AdditionalProjects.Select(p => p.Value.Language)).Distinct().Count(); + } + } + + if (NumberOfFixAllInProjectIterations != null) + { + numberOfFixAllInProjectIterations = NumberOfFixAllInProjectIterations.Value; + } + else if (NumberOfFixAllIterations != null) + { + numberOfFixAllInProjectIterations = NumberOfFixAllIterations.Value; + } + else + { + numberOfFixAllInProjectIterations = 0; + if (HasAnyChange(testState, batchFixedState, recursive: false)) + { + // Expect at most one iteration for a fixable primary project + numberOfFixAllInProjectIterations--; + } + + foreach (var (name, state) in testState.AdditionalProjects) + { + if (!batchFixedState.AdditionalProjects.TryGetValue(name, out var expected) + || HasAnyChange(state, expected, recursive: true)) + { + // Expect at most one iteration for each fixable additional project + numberOfFixAllInProjectIterations--; + } } } @@ -314,15 +373,13 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, batchFixedState)) + if (!HasAnyChange(testState, batchFixedState, recursive: false)) { numberOfFixAllInDocumentIterations = 0; } else { // Expect at most one iteration per fixable document - var fixers = GetCodeFixProviders().ToArray(); - var fixableDiagnostics = testState.ExpectedDiagnostics.Where(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))); numberOfFixAllInDocumentIterations = -fixableDiagnostics.GroupBy(diagnostic => diagnostic.Spans.FirstOrDefault().Span.Path).Count(); } } @@ -352,7 +409,7 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed var t3 = CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.SkipFixAllInProjectCheck) ? ((Task)Task.FromResult(true)).ConfigureAwait(false) - : VerifyFixAsync(Language, GetDiagnosticAnalyzers().ToImmutableArray(), GetCodeFixProviders().ToImmutableArray(), testState, batchFixedState, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInProjectAsync, verifier.PushContext("Fix all in project"), cancellationToken).ConfigureAwait(false); + : VerifyFixAsync(Language, GetDiagnosticAnalyzers().ToImmutableArray(), GetCodeFixProviders().ToImmutableArray(), testState, batchFixedState, numberOfFixAllInProjectIterations, FixAllAnalyzerDiagnosticsInProjectAsync, verifier.PushContext("Fix all in project"), cancellationToken).ConfigureAwait(false); if (Debugger.IsAttached) { await t3; @@ -404,6 +461,21 @@ private async Task VerifyFixAsync( ExceptionDispatchInfo? iterationCountFailure; (project, iterationCountFailure) = await getFixedProject(analyzers, codeFixProviders, CodeActionIndex, CodeActionEquivalenceKey, CodeActionVerifier, project, numberOfIterations, verifier, cancellationToken).ConfigureAwait(false); + // After applying all of the code fixes, compare the resulting string to the inputted one + await VerifyProjectAsync(newState, project, verifier, cancellationToken).ConfigureAwait(false); + + foreach (var additionalProject in newState.AdditionalProjects) + { + var actualProject = project.Solution.Projects.Single(p => p.Name == additionalProject.Key); + await VerifyProjectAsync(additionalProject.Value, actualProject, verifier, cancellationToken); + } + + // Validate the iteration counts after validating the content + iterationCountFailure?.Throw(); + } + + private async Task VerifyProjectAsync(ProjectState newState, Project project, IVerifier verifier, CancellationToken cancellationToken) + { // After applying all of the code fixes, compare the resulting string to the inputted one var updatedDocuments = project.Documents.ToArray(); @@ -443,13 +515,17 @@ private async Task VerifyFixAsync( verifier.Equal(newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); verifier.Equal(newState.AnalyzerConfigFiles[i].filename, updatedAnalyzerConfigDocuments[i].Name, $"file name was expected to be '{newState.AnalyzerConfigFiles[i].filename}' but was '{updatedAnalyzerConfigDocuments[i].Name}'"); } - - // Validate the iteration counts after validating the content - iterationCountFailure?.Throw(); } private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> FixEachAnalyzerDiagnosticAsync(ImmutableArray analyzers, ImmutableArray codeFixProviders, int? codeFixIndex, string? codeFixEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { @@ -485,7 +561,7 @@ private async Task VerifyFixAsync( var fixableDiagnostics = analyzerDiagnostics .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.Id))) - .Where(diagnostic => project.GetDocument(diagnostic.Location.SourceTree) is object) + .Where(diagnostic => project.Solution.GetDocument(diagnostic.Location.SourceTree) is object) .ToImmutableArray(); if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) @@ -500,6 +576,7 @@ private async Task VerifyFixAsync( { var actions = ImmutableArray.CreateBuilder(); + var fixableDocument = project.Solution.GetDocument(diagnostic.Location.SourceTree); foreach (var codeFixProvider in codeFixProviders) { if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) @@ -508,7 +585,7 @@ private async Task VerifyFixAsync( continue; } - var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); + var context = new CodeFixContext(fixableDocument, diagnostic, (a, d) => actions.Add(a), cancellationToken); await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); } @@ -518,11 +595,12 @@ private async Task VerifyFixAsync( { anyActions = true; - var fixedProject = await ApplyCodeActionAsync(project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(fixableDocument.Project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != fixableDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); break; } } @@ -579,6 +657,13 @@ private async Task VerifyFixAsync( private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope scope, ImmutableArray analyzers, ImmutableArray codeFixProviders, int? codeFixIndex, string? codeFixEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { @@ -620,7 +705,7 @@ private async Task VerifyFixAsync( foreach (var codeFixProvider in codeFixProviders) { if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id) - || !(project.GetDocument(diagnostic.Location.SourceTree) is { } document)) + || !(project.Solution.GetDocument(diagnostic.Location.SourceTree) is { } document)) { // do not pass unsupported diagnostics to a code fix provider continue; @@ -655,11 +740,12 @@ private async Task VerifyFixAsync( FixAllContext.DiagnosticProvider fixAllDiagnosticProvider = TestDiagnosticProvider.Create(analyzerDiagnostics); + var fixableDocument = project.Solution.GetDocument(firstDiagnostic.Location.SourceTree); var analyzerDiagnosticIds = analyzers.SelectMany(x => x.SupportedDiagnostics).Select(x => x.Id); var compilerDiagnosticIds = codeFixProviders.SelectMany(codeFixProvider => codeFixProvider.FixableDiagnosticIds).Where(x => x.StartsWith("CS", StringComparison.Ordinal) || x.StartsWith("BC", StringComparison.Ordinal)); var disabledDiagnosticIds = project.CompilationOptions.SpecificDiagnosticOptions.Where(x => x.Value == ReportDiagnostic.Suppress).Select(x => x.Key); var relevantIds = analyzerDiagnosticIds.Concat(compilerDiagnosticIds).Except(disabledDiagnosticIds).Distinct(); - var fixAllContext = new FixAllContext(project.GetDocument(firstDiagnostic.Location.SourceTree), effectiveCodeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); + var fixAllContext = new FixAllContext(fixableDocument, effectiveCodeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); var action = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false); if (action == null) @@ -667,11 +753,12 @@ private async Task VerifyFixAsync( return (project, null); } - var fixedProject = await ApplyCodeActionAsync(project, action, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(fixableDocument.Project, action, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != fixableDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); } if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt index 6e09256b2..1552a9842 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt @@ -14,6 +14,8 @@ Microsoft.CodeAnalysis.Testing.CodeFixTest.FixedCode.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.FixedState.get -> Microsoft.CodeAnalysis.Testing.SolutionState Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInDocumentIterations.get -> int? Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInDocumentIterations.set -> void +Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInProjectIterations.get -> int? +Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInProjectIterations.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllIterations.get -> int? Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllIterations.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfIncrementalIterations.get -> int? diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs index 3b2e0def1..80ee3581f 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs @@ -114,7 +114,7 @@ private bool CodeActionExpected() return CodeActionExpected(FixedState); } - protected override DiagnosticDescriptor? GetDefaultDiagnostic(DiagnosticAnalyzer[] analyzers) + protected internal override DiagnosticDescriptor? GetDefaultDiagnostic(DiagnosticAnalyzer[] analyzers) { if (base.GetDefaultDiagnostic(analyzers) is { } descriptor) { @@ -135,7 +135,7 @@ private bool CodeActionExpected() /// A representing the asynchronous operation. protected async Task VerifyRefactoringAsync(SolutionState testState, SolutionState fixedState, DiagnosticResult triggerSpan, IVerifier verifier, CancellationToken cancellationToken) { - var numberOfIncrementalIterations = OffersEmptyRefactoring || HasAnyChange(testState, fixedState) ? 1 : 0; + var numberOfIncrementalIterations = OffersEmptyRefactoring || HasAnyChange(testState, fixedState, recursive: true) ? 1 : 0; await VerifyRefactoringAsync(Language, triggerSpan, GetCodeRefactoringProviders().ToImmutableArray(), testState, fixedState, numberOfIncrementalIterations, ApplyRefactoringAsync, verifier.PushContext("Code refactoring application"), cancellationToken); } @@ -156,6 +156,21 @@ private async Task VerifyRefactoringAsync( ExceptionDispatchInfo? iterationCountFailure; (project, iterationCountFailure) = await getFixedProject(triggerSpan, codeRefactoringProviders, CodeActionIndex, CodeActionEquivalenceKey, CodeActionVerifier, project, numberOfIterations, verifier, cancellationToken).ConfigureAwait(false); + // After applying the refactoring, compare the resulting string to the inputted one + await VerifyProjectAsync(newState, project, verifier, cancellationToken).ConfigureAwait(false); + + foreach (var additionalProject in newState.AdditionalProjects) + { + var actualProject = project.Solution.Projects.Single(p => p.Name == additionalProject.Key); + await VerifyProjectAsync(additionalProject.Value, actualProject, verifier, cancellationToken); + } + + // Validate the iteration counts after validating the content + iterationCountFailure?.Throw(); + } + + private async Task VerifyProjectAsync(ProjectState newState, Project project, IVerifier verifier, CancellationToken cancellationToken) + { // After applying the refactoring, compare the resulting string to the inputted one var updatedDocuments = project.Documents.ToArray(); @@ -183,12 +198,29 @@ private async Task VerifyRefactoringAsync( verifier.Equal(newState.AdditionalFiles[i].filename, updatedAdditionalDocuments[i].Name, $"file name was expected to be '{newState.AdditionalFiles[i].filename}' but was '{updatedAdditionalDocuments[i].Name}'"); } - // Validate the iteration counts after validating the content - iterationCountFailure?.Throw(); + var updatedAnalyzerConfigDocuments = project.AnalyzerConfigDocuments().ToArray(); + + verifier.Equal(newState.AnalyzerConfigFiles.Count, updatedAnalyzerConfigDocuments.Length, $"expected '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' and '{nameof(updatedAnalyzerConfigDocuments)}' to be equal but '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' contains '{newState.AnalyzerConfigFiles.Count}' documents and '{nameof(updatedAnalyzerConfigDocuments)}' contains '{updatedAnalyzerConfigDocuments.Length}' documents"); + + for (var i = 0; i < updatedAnalyzerConfigDocuments.Length; i++) + { + var actual = await updatedAnalyzerConfigDocuments[i].GetTextAsync(cancellationToken).ConfigureAwait(false); + verifier.EqualOrDiff(newState.AnalyzerConfigFiles[i].content.ToString(), actual.ToString(), $"content of '{newState.AnalyzerConfigFiles[i].filename}' did not match. Diff shown with expected as baseline:"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.Encoding, actual.Encoding, $"encoding of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.Encoding?.WebName}' but was '{actual.Encoding?.WebName}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].filename, updatedAnalyzerConfigDocuments[i].Name, $"file name was expected to be '{newState.AnalyzerConfigFiles[i].filename}' but was '{updatedAnalyzerConfigDocuments[i].Name}'"); + } } private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> ApplyRefactoringAsync(DiagnosticResult triggerSpan, ImmutableArray codeRefactoringProviders, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { @@ -212,10 +244,11 @@ private async Task VerifyRefactoringAsync( var actions = ImmutableArray.CreateBuilder(); var location = await GetTriggerLocationAsync(); + var triggerDocument = project.Solution.GetDocument(location.SourceTree); foreach (var codeRefactoringProvider in codeRefactoringProviders) { - var context = new CodeRefactoringContext(project.GetDocument(location.SourceTree), location.SourceSpan, actions.Add, cancellationToken); + var context = new CodeRefactoringContext(triggerDocument, location.SourceSpan, actions.Add, cancellationToken); await codeRefactoringProvider.ComputeRefactoringsAsync(context).ConfigureAwait(false); } @@ -225,11 +258,12 @@ private async Task VerifyRefactoringAsync( { anyActions = true; - var fixedProject = await ApplyCodeActionAsync(project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(triggerDocument.Project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != triggerDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); break; } } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs index 43704cacf..5b38386b7 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs @@ -3,18 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Composition; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.TestAnalyzers; using Microsoft.CodeAnalysis.Testing.TestFixes; -using Microsoft.CodeAnalysis.Text; using Xunit; namespace Microsoft.CodeAnalysis.Testing @@ -220,11 +212,11 @@ class TestClass { }.RunAsync(); }); - new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}The upper limit for the number of code fix iterations was exceeded", exception.Message); + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}Expected '1' iterations but found '2' iterations.", exception.Message); } [Theory] - [InlineData(-1, "The upper limit for the number of code fix iterations was exceeded", " 5")] + [InlineData(-1, "Expected '1' iterations but found '2' iterations.", " 5")] [InlineData(0, "The upper limit for the number of code fix iterations was exceeded", " [|4|]")] [InlineData(1, "Expected '1' iterations but found '2' iterations.", " 5")] public async Task TestTwoIterationsRequiredButIncrementalDeclaredIncorrectly(int declaredIncrementalIterations, string message, string replacement) @@ -284,11 +276,11 @@ class TestClass { }.RunAsync(); }); - Assert.Equal($"Context: Fix all in document{Environment.NewLine}The upper limit for the number of code fix iterations was exceeded", exception.Message); + Assert.Equal($"Context: Fix all in document{Environment.NewLine}Expected '1' iterations but found '2' iterations.", exception.Message); } [Theory] - [InlineData(-1, "The upper limit for the number of code fix iterations was exceeded", " 5")] + [InlineData(-1, "Expected '1' iterations but found '2' iterations.", " 5")] [InlineData(0, "The upper limit for the number of fix all iterations was exceeded", " [|4|]")] [InlineData(1, "Expected '1' iterations but found '2' iterations.", " 5")] public async Task TestTwoIterationsRequiredButFixAllDeclaredIncorrectly(int declaredFixAllIterations, string message, string replacement) @@ -403,69 +395,6 @@ class TestClass2 { Assert.Equal($"Context: {context}{Environment.NewLine}Expected '2' iterations but found '1' iterations.", exception.Message); } - /// - /// Reports a diagnostic on any integer literal token with a value less than five. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - private class LiteralUnderFiveAnalyzer : DiagnosticAnalyzer - { - internal static readonly DiagnosticDescriptor Descriptor = - new DiagnosticDescriptor("LiteralUnderFive", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - - context.RegisterSyntaxNodeAction(HandleNumericLiteralExpression, SyntaxKind.NumericLiteralExpression); - } - - private void HandleNumericLiteralExpression(SyntaxNodeAnalysisContext context) - { - var node = (LiteralExpressionSyntax)context.Node; - if (int.TryParse(node.Token.ValueText, out var value) && value < 5) - { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, node.Token.GetLocation())); - } - } - } - - [ExportCodeFixProvider(LanguageNames.CSharp)] - [PartNotDiscoverable] - private class IncrementFix : CodeFixProvider - { - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(LiteralUnderFiveAnalyzer.Descriptor.Id); - - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; - - public override Task RegisterCodeFixesAsync(CodeFixContext context) - { - foreach (var diagnostic in context.Diagnostics) - { - context.RegisterCodeFix( - CodeAction.Create( - "LiteralUnderFive", - cancellationToken => CreateChangedDocument(context.Document, diagnostic.Location.SourceSpan, cancellationToken), - nameof(IncrementFix)), - diagnostic); - } - - return Task.CompletedTask; - } - - private async Task CreateChangedDocument(Document document, TextSpan sourceSpan, CancellationToken cancellationToken) - { - var tree = (await document.GetSyntaxTreeAsync(cancellationToken))!; - var root = await tree.GetRootAsync(cancellationToken); - var token = root.FindToken(sourceSpan.Start); - var replacement = int.Parse(token.ValueText) + 1; - var newToken = SyntaxFactory.Literal(token.LeadingTrivia, " " + replacement.ToString(), replacement, token.TrailingTrivia); - return document.WithSyntaxRoot(root.ReplaceToken(token, newToken)); - } - } - private class CSharpTest : CSharpCodeFixTest { public int DiagnosticIndexToFix { get; set; } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs new file mode 100644 index 000000000..35059a454 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Xunit; +using CSharpTest = Microsoft.CodeAnalysis.Testing.TestFixes.CSharpCodeFixTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.LiteralUnderFiveAnalyzer, + Microsoft.CodeAnalysis.Testing.TestFixes.IncrementFix>; +using VisualBasicTest = Microsoft.CodeAnalysis.Testing.TestFixes.VisualBasicCodeFixTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.LiteralUnderFiveAnalyzer, + Microsoft.CodeAnalysis.Testing.TestFixes.IncrementFix>; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class FixMultipleProjectsTests + { + [Fact] + public async Task TwoCSharpProjects_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TwoVisualBasicProjects_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field = [|4|] : End Class", + @"Public Class Type2 : Private field = [|4|] : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Type3 : Private field = [|4|] : End Class", + @"Public Class Type4 : Private field = [|4|] : End Class", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"Public Class Type1 : Private field = 5 : End Class", + @"Public Class Type2 : Private field = 5 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Type3 : Private field = 5 : End Class", + @"Public Class Type4 : Private field = 5 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field = [|4|] : End Class", + @"Public Class Type4 : Private field = [|4|] : End Class", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field = 5 : End Class", + @"Public Class Type4 : Private field = 5 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field = [|4|] : End Class", + @"Public Class Type2 : Private field = [|4|] : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"Public Class Type1 : Private field = 5 : End Class", + @"Public Class Type2 : Private field = 5 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TwoCSharpProjects_Independent_UnexpectedDiagnostic() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|5|]; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + MarkupHandling = MarkupMode.Allow, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Diagnostics of fixed state{Environment.NewLine}Mismatch between number of diagnostics returned, expected \"1\" actual \"0\"{Environment.NewLine}{Environment.NewLine}Diagnostics:{Environment.NewLine} NONE.{Environment.NewLine}", exception.Message); + } + + [Fact] + public async Task TwoCSharpProjects_Independent_UnexpectedContent() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}content of '/Secondary/Test0.cs' did not match. Diff shown with expected as baseline:{Environment.NewLine}-public class Type3 {{ int field = 5; }}{Environment.NewLine}+public class Type3 {{ int field = 5; }}{Environment.NewLine}", exception.Message); + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs index b47c232dc..03780afb8 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs @@ -290,6 +290,68 @@ public async Task TestNoValidationPassesFull() }.RunAsync(); } + [Fact] + [WorkItem(806, "https://github.com/dotnet/roslyn-sdk/issues/806")] + public async Task TestWithAdditionalProject_SameLanguage() + { + await new ReplaceThisWithBaseTest + { + TestState = + { + Sources = { "public class Ignored { }" }, + AdditionalProjects = + { + ["Additional"] = + { + Sources = { ReplaceThisWithBaseTestCode }, + }, + }, + }, + FixedState = + { + Sources = { "public class Ignored { }" }, + AdditionalProjects = + { + ["Additional"] = + { + Sources = { ReplaceThisWithBaseFixedCode }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + [WorkItem(806, "https://github.com/dotnet/roslyn-sdk/issues/806")] + public async Task TestWithAdditionalProject_DifferentLanguage() + { + await new ReplaceThisWithBaseTestVisualBasic + { + TestState = + { + Sources = { "Public Class Ignored : End Class" }, + AdditionalProjects = + { + ["Additional", LanguageNames.CSharp] = + { + Sources = { ReplaceThisWithBaseTestCode }, + }, + }, + }, + FixedState = + { + Sources = { "Public Class Ignored : End Class" }, + AdditionalProjects = + { + ["Additional", LanguageNames.CSharp] = + { + Sources = { ReplaceThisWithBaseFixedCode }, + }, + }, + }, + }.RunAsync(); + } + [ExportCodeRefactoringProvider(LanguageNames.CSharp)] [PartNotDiscoverable] private class ReplaceThisWithBaseTokenFix : CodeRefactoringProvider @@ -405,5 +467,30 @@ protected override IEnumerable GetCodeRefactoringProvid yield return new TCodeRefactoring(); } } + + private class ReplaceThisWithBaseTestVisualBasic : CodeRefactoringTest + where TCodeRefactoring : CodeRefactoringProvider, new() + { + public override string Language => LanguageNames.VisualBasic; + + public override Type SyntaxKindType => typeof(VisualBasic.SyntaxKind); + + protected override string DefaultFileExt => "vb"; + + protected override CompilationOptions CreateCompilationOptions() + { + return new VisualBasic.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + } + + protected override ParseOptions CreateParseOptions() + { + return new VisualBasic.VisualBasicParseOptions(VisualBasic.LanguageVersion.Default, DocumentationMode.Diagnose); + } + + protected override IEnumerable GetCodeRefactoringProviders() + { + yield return new TCodeRefactoring(); + } + } } } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj index 614d81005..184f5e0fe 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj @@ -36,5 +36,6 @@ + diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs new file mode 100644 index 000000000..281dd7808 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.CodeAnalysis.Testing.TestAnalyzers +{ + /// + /// Reports a diagnostic on any integer literal with a value less than five. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class LiteralUnderFiveAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("LiteralUnderFive", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterOperationAction(HandleLiteralOperation, OperationKind.Literal); + } + + private void HandleLiteralOperation(OperationAnalysisContext context) + { + var operation = (ILiteralOperation)context.Operation; + if (operation.ConstantValue.HasValue + && operation.ConstantValue.Value is int value + && value < 5) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, operation.Syntax.GetLocation())); + } + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs new file mode 100644 index 000000000..dadff19f9 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Testing.TestAnalyzers; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.Testing.TestFixes +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] + [PartNotDiscoverable] + public class IncrementFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(LiteralUnderFiveAnalyzer.Descriptor.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + "LiteralUnderFive", + cancellationToken => CreateChangedDocument(context.Document, diagnostic.Location.SourceSpan, cancellationToken), + nameof(IncrementFix)), + diagnostic); + } + + return Task.CompletedTask; + } + + private async Task CreateChangedDocument(Document document, TextSpan sourceSpan, CancellationToken cancellationToken) + { + var tree = (await document.GetSyntaxTreeAsync(cancellationToken))!; + var root = await tree.GetRootAsync(cancellationToken); + var token = root.FindToken(sourceSpan.Start); + var replacement = int.Parse(token.ValueText) + 1; + var generator = SyntaxGenerator.GetGenerator(document); + + var originalLeadingTrivia = token.LeadingTrivia; + SyntaxTriviaList newLeadingTrivia; + Assert.Equal(0, originalLeadingTrivia.Count); + if (document.Project.Language == LanguageNames.CSharp) + { + newLeadingTrivia = CSharp.SyntaxFactory.TriviaList(CSharp.SyntaxFactory.Space); + } + else + { + newLeadingTrivia = VisualBasic.SyntaxFactory.TriviaList(VisualBasic.SyntaxFactory.Space); + } + + var newExpression = generator.LiteralExpression(replacement).WithLeadingTrivia(newLeadingTrivia).WithTrailingTrivia(token.TrailingTrivia); + return document.WithSyntaxRoot(root.ReplaceNode(token.Parent!, newExpression)); + } + } +} From c7a834711102ed9329486fb586cf920236a1b565 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 21 Jun 2021 22:10:43 -0700 Subject: [PATCH 3/3] Track projects where diagnostics are reported --- .../AnalyzerTest`1.cs | 78 ++++++++++--------- .../PublicAPI.Unshipped.txt | 2 +- .../CodeFixTest`1.cs | 27 +++---- .../TestDiagnosticProvider.cs | 12 +-- 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs index e17ac7b5d..6410ab7e1 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs @@ -342,19 +342,19 @@ private async Task VerifySuppressionDiagnosticsAsync(ImmutableArrayA collection of s describing the expected /// diagnostics for the sources. /// The verifier to use for test assertions. - private void VerifyDiagnosticResults(IEnumerable actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults, IVerifier verifier) + private void VerifyDiagnosticResults(IEnumerable<(Project project, Diagnostic diagnostic)> actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults, IVerifier verifier) { var matchedDiagnostics = MatchDiagnostics(actualResults.ToArray(), expectedResults); verifier.Equal(actualResults.Count(), matchedDiagnostics.Count(x => x.actual is object), $"{nameof(MatchDiagnostics)} failed to include all actual diagnostics in the result"); verifier.Equal(expectedResults.Length, matchedDiagnostics.Count(x => x.expected is object), $"{nameof(MatchDiagnostics)} failed to include all expected diagnostics in the result"); - actualResults = matchedDiagnostics.Select(x => x.actual).WhereNotNull(); + actualResults = matchedDiagnostics.Select(x => x.actual).Where(x => x is { }).Select(x => x!.Value); expectedResults = matchedDiagnostics.Where(x => x.expected is object).Select(x => x.expected.GetValueOrDefault()).ToArray(); var expectedCount = expectedResults.Length; var actualCount = actualResults.Count(); - var diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzers, DefaultFilePath, actualResults.ToArray()) : " NONE."; + var diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzers, DefaultFilePath, actualResults.Select(result => result.diagnostic).ToArray()) : " NONE."; var message = $"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"\r\n\r\nDiagnostics:\r\n{diagnosticsOutput}\r\n"; verifier.Equal(expectedCount, actualCount, message); @@ -365,51 +365,51 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu if (!expected.HasLocation) { - message = FormatVerifierMessage(analyzers, actual, expected, "Expected a project diagnostic with no location:"); - verifier.Equal(Location.None, actual.Location, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, "Expected a project diagnostic with no location:"); + verifier.Equal(Location.None, actual.diagnostic.Location, message); } else { - VerifyDiagnosticLocation(analyzers, actual, expected, actual.Location, expected.Spans[0], verifier); + VerifyDiagnosticLocation(analyzers, actual.diagnostic, expected, actual.diagnostic.Location, expected.Spans[0], verifier); if (!expected.Options.HasFlag(DiagnosticOptions.IgnoreAdditionalLocations)) { - var additionalLocations = actual.AdditionalLocations.ToArray(); + var additionalLocations = actual.diagnostic.AdditionalLocations.ToArray(); - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected {expected.Spans.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:"); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected {expected.Spans.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:"); verifier.Equal(expected.Spans.Length - 1, additionalLocations.Length, message); for (var j = 0; j < additionalLocations.Length; ++j) { - VerifyDiagnosticLocation(analyzers, actual, expected, additionalLocations[j], expected.Spans[j + 1], verifier); + VerifyDiagnosticLocation(analyzers, actual.diagnostic, expected, additionalLocations[j], expected.Spans[j + 1], verifier); } } } - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.Id}\""); - verifier.Equal(expected.Id, actual.Id, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.diagnostic.Id}\""); + verifier.Equal(expected.Id, actual.diagnostic.Id, message); if (!expected.Options.HasFlag(DiagnosticOptions.IgnoreSeverity)) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\""); - verifier.Equal(expected.Severity, actual.Severity, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.diagnostic.Severity}\""); + verifier.Equal(expected.Severity, actual.diagnostic.Severity, message); } if (expected.Message != null) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.GetMessage()}\""); - verifier.Equal(expected.Message, actual.GetMessage(), message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.diagnostic.GetMessage()}\""); + verifier.Equal(expected.Message, actual.diagnostic.GetMessage(), message); } else if (expected.MessageArguments?.Length > 0) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic message arguments to match"); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic message arguments to match"); verifier.SequenceEqual( expected.MessageArguments.Select(argument => argument?.ToString() ?? string.Empty), - GetArguments(actual).Select(argument => argument?.ToString() ?? string.Empty), + GetArguments(actual.diagnostic).Select(argument => argument?.ToString() ?? string.Empty), StringComparer.Ordinal, message); } - DiagnosticVerifier?.Invoke(actual, expected, verifier); + DiagnosticVerifier?.Invoke(actual.diagnostic, expected, verifier); } } @@ -441,11 +441,11 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu /// the total number of mismatched pairs. /// /// - private ImmutableArray<(Diagnostic? actual, DiagnosticResult? expected)> MatchDiagnostics(Diagnostic[] actualResults, DiagnosticResult[] expectedResults) + private ImmutableArray<((Project project, Diagnostic diagnostic)? actual, DiagnosticResult? expected)> MatchDiagnostics((Project project, Diagnostic diagnostic)[] actualResults, DiagnosticResult[] expectedResults) { - var actualIds = actualResults.Select(result => result.Id).ToImmutableArray(); - var actualResultLocations = actualResults.Select(result => (location: result.Location.GetLineSpan(), additionalLocations: result.AdditionalLocations.Select(location => location.GetLineSpan()).ToImmutableArray())).ToImmutableArray(); - var actualArguments = actualResults.Select(actual => GetArguments(actual).Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray()).ToImmutableArray(); + var actualIds = actualResults.Select(result => result.diagnostic.Id).ToImmutableArray(); + var actualResultLocations = actualResults.Select(result => (location: result.diagnostic.Location.GetLineSpan(), additionalLocations: result.diagnostic.AdditionalLocations.Select(location => location.GetLineSpan()).ToImmutableArray())).ToImmutableArray(); + var actualArguments = actualResults.Select(actual => GetArguments(actual.diagnostic).Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray()).ToImmutableArray(); expectedResults = expectedResults.ToOrderedArray(); var expectedArguments = expectedResults.Select(expected => expected.MessageArguments?.Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray() ?? ImmutableArray.Empty).ToImmutableArray(); @@ -453,9 +453,9 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu // Initialize the best match to a trivial result where everything is unmatched. This will be updated if/when // better matches are found. var bestMatchCount = MatchQuality.RemainingUnmatched(actualResults.Length + expectedResults.Length); - var bestMatch = actualResults.Select(result => ((Diagnostic?)result, default(DiagnosticResult?))).Concat(expectedResults.Select(result => (default(Diagnostic?), (DiagnosticResult?)result))).ToImmutableArray(); + var bestMatch = actualResults.Select(result => (((Project project, Diagnostic diagnostic)?)result, default(DiagnosticResult?))).Concat(expectedResults.Select(result => (default((Project project, Diagnostic diagnostic)?), (DiagnosticResult?)result))).ToImmutableArray(); - var builder = ImmutableArray.CreateBuilder<(Diagnostic? actual, DiagnosticResult? expected)>(); + var builder = ImmutableArray.CreateBuilder<((Project project, Diagnostic diagnostic)? actual, DiagnosticResult? expected)>(); var usedExpected = new bool[expectedResults.Length]; // The recursive match algorithm is not optimized, so use a timeout to ensure it completes in a reasonable @@ -531,7 +531,7 @@ MatchQuality RecursiveMatch(int firstActualIndex, int remainingActualItems, int } var (lineSpan, additionalLineSpans) = actualResultLocations[firstActualIndex]; - var matchValue = GetMatchValue(actualResults[firstActualIndex], actualIds[firstActualIndex], lineSpan, additionalLineSpans, actualArguments[firstActualIndex], expectedResults[i], expectedArguments[i]); + var matchValue = GetMatchValue(actualResults[firstActualIndex].diagnostic, actualIds[firstActualIndex], lineSpan, additionalLineSpans, actualArguments[firstActualIndex], expectedResults[i], expectedArguments[i]); if (matchValue == MatchQuality.None) { continue; @@ -992,13 +992,15 @@ private static bool IsInSourceFile(DiagnosticResult result, (string filename, So /// The that the task will observe. /// A collection of s that surfaced in the source code, sorted by /// . - private async Task> GetSortedDiagnosticsAsync(EvaluatedProjectState primaryProject, ImmutableArray additionalProjects, ImmutableArray analyzers, IVerifier verifier, CancellationToken cancellationToken) + private async Task> GetSortedDiagnosticsAsync(EvaluatedProjectState primaryProject, ImmutableArray additionalProjects, ImmutableArray analyzers, IVerifier verifier, CancellationToken cancellationToken) { var solution = await GetSolutionAsync(primaryProject, additionalProjects, verifier, cancellationToken); - var additionalDiagnostics = primaryProject.AdditionalDiagnostics; - foreach (var project in additionalProjects) + var primaryProjectInSolution = solution.Projects.Single(project => project.Name == DefaultTestProjectName); + var additionalDiagnostics = primaryProject.AdditionalDiagnostics.Select(diagnostic => (primaryProjectInSolution, diagnostic)).ToImmutableArray(); + foreach (var additionalProject in additionalProjects) { - additionalDiagnostics = additionalDiagnostics.AddRange(project.AdditionalDiagnostics); + var additionalProjectInSolution = solution.Projects.Single(project => project.Name == additionalProject.Name); + additionalDiagnostics = additionalDiagnostics.AddRange(additionalProject.AdditionalDiagnostics.Select(diagnostic => (additionalProjectInSolution, diagnostic))); } return await GetSortedDiagnosticsAsync(solution, analyzers, additionalDiagnostics, CompilerDiagnostics, verifier, cancellationToken); @@ -1016,21 +1018,21 @@ private async Task> GetSortedDiagnosticsAsync(Evaluat /// The that the task will observe. /// A collection of s that surfaced in the source code, sorted by /// . - protected async Task> GetSortedDiagnosticsAsync(Solution solution, ImmutableArray analyzers, ImmutableArray additionalDiagnostics, CompilerDiagnostics compilerDiagnostics, IVerifier verifier, CancellationToken cancellationToken) + protected async Task> GetSortedDiagnosticsAsync(Solution solution, ImmutableArray analyzers, ImmutableArray<(Project project, Diagnostic diagnostic)> additionalDiagnostics, CompilerDiagnostics compilerDiagnostics, IVerifier verifier, CancellationToken cancellationToken) { if (analyzers.IsEmpty) { analyzers = ImmutableArray.Create(new EmptyDiagnosticAnalyzer()); } - var diagnostics = ImmutableArray.CreateBuilder(); + var diagnostics = ImmutableArray.CreateBuilder<(Project project, Diagnostic diagnostic)>(); foreach (var project in solution.Projects) { var compilation = await GetProjectCompilationAsync(project, verifier, cancellationToken).ConfigureAwait(false); var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, GetAnalyzerOptions(project), cancellationToken); var allDiagnostics = await compilationWithAnalyzers.GetAllDiagnosticsAsync().ConfigureAwait(false); - diagnostics.AddRange(allDiagnostics.Where(diagnostic => !IsCompilerDiagnostic(diagnostic) || IsCompilerDiagnosticIncluded(diagnostic, compilerDiagnostics))); + diagnostics.AddRange(allDiagnostics.Where(diagnostic => !IsCompilerDiagnostic(diagnostic) || IsCompilerDiagnosticIncluded(diagnostic, compilerDiagnostics)).Select(diagnostic => (project, diagnostic))); } diagnostics.AddRange(additionalDiagnostics); @@ -1357,14 +1359,14 @@ protected virtual Workspace CreateWorkspaceImpl() /// A collection of s to be sorted. /// A collection containing the input , sorted by /// and . - private static Diagnostic[] SortDistinctDiagnostics(IEnumerable diagnostics) + private static (Project project, Diagnostic diagnostic)[] SortDistinctDiagnostics(IEnumerable<(Project project, Diagnostic diagnostic)> diagnostics) { return diagnostics - .OrderBy(d => d.Location.GetLineSpan().Path, StringComparer.Ordinal) - .ThenBy(d => d.Location.SourceSpan.Start) - .ThenBy(d => d.Location.SourceSpan.End) - .ThenBy(d => d.Id) - .ThenBy(d => GetArguments(d), LexicographicComparer.Instance).ToArray(); + .OrderBy(d => d.diagnostic.Location.GetLineSpan().Path, StringComparer.Ordinal) + .ThenBy(d => d.diagnostic.Location.SourceSpan.Start) + .ThenBy(d => d.diagnostic.Location.SourceSpan.End) + .ThenBy(d => d.diagnostic.Id) + .ThenBy(d => GetArguments(d.diagnostic), LexicographicComparer.Instance).ToArray(); } private static IReadOnlyList GetArguments(Diagnostic diagnostic) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt index 14d5b1ccb..42f6732ca 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt @@ -9,7 +9,7 @@ Microsoft.CodeAnalysis.Testing.AnalyzerTest.DiagnosticVerifier.set -> Microsoft.CodeAnalysis.Testing.AnalyzerTest.DisabledDiagnostics.get -> System.Collections.Generic.List Microsoft.CodeAnalysis.Testing.AnalyzerTest.ExpectedDiagnostics.get -> System.Collections.Generic.List Microsoft.CodeAnalysis.Testing.AnalyzerTest.FormatVerifierMessage(System.Collections.Immutable.ImmutableArray analyzers, Microsoft.CodeAnalysis.Diagnostic actual, Microsoft.CodeAnalysis.Testing.DiagnosticResult expected, string message) -> string -Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetSortedDiagnosticsAsync(Microsoft.CodeAnalysis.Solution solution, System.Collections.Immutable.ImmutableArray analyzers, System.Collections.Immutable.ImmutableArray additionalDiagnostics, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetSortedDiagnosticsAsync(Microsoft.CodeAnalysis.Solution solution, System.Collections.Immutable.ImmutableArray analyzers, System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> additionalDiagnostics, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> Microsoft.CodeAnalysis.Testing.AnalyzerTest.MarkupOptions.get -> Microsoft.CodeAnalysis.Testing.MarkupOptions Microsoft.CodeAnalysis.Testing.AnalyzerTest.MarkupOptions.set -> void Microsoft.CodeAnalysis.Testing.AnalyzerTest.MatchDiagnosticsTimeout.get -> System.TimeSpan diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs index de2c8c122..ff1d2c065 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs @@ -532,12 +532,12 @@ private async Task VerifyProjectAsync(ProjectState newState, Project project, IV numberOfIterations = -numberOfIterations; } - var previousDiagnostics = ImmutableArray.Create(); + var previousDiagnostics = ImmutableArray.Create<(Project project, Diagnostic diagnostic)>(); bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); + var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -560,19 +560,19 @@ private async Task VerifyProjectAsync(ProjectState newState, Project project, IV previousDiagnostics = analyzerDiagnostics; var fixableDiagnostics = analyzerDiagnostics - .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.Id))) - .Where(diagnostic => project.Solution.GetDocument(diagnostic.Location.SourceTree) is object) + .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.diagnostic.Id))) + .Where(diagnostic => project.Solution.GetDocument(diagnostic.diagnostic.Location.SourceTree) is object) .ToImmutableArray(); if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) { - var diagnosticToFix = TrySelectDiagnosticToFix(fixableDiagnostics); - fixableDiagnostics = diagnosticToFix is object ? ImmutableArray.Create(diagnosticToFix) : ImmutableArray.Empty; + var diagnosticToFix = TrySelectDiagnosticToFix(fixableDiagnostics.Select(x => x.diagnostic).ToImmutableArray()); + fixableDiagnostics = diagnosticToFix is object ? ImmutableArray.Create(fixableDiagnostics.Single(x => x.diagnostic == diagnosticToFix)) : ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty; } done = true; var anyActions = false; - foreach (var diagnostic in fixableDiagnostics) + foreach (var (_, diagnostic) in fixableDiagnostics) { var actions = ImmutableArray.CreateBuilder(); @@ -670,12 +670,12 @@ private async Task VerifyProjectAsync(ProjectState newState, Project project, IV numberOfIterations = -numberOfIterations; } - var previousDiagnostics = ImmutableArray.Create(); + var previousDiagnostics = ImmutableArray.Create<(Project project, Diagnostic diagnostic)>(); bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); + var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -698,7 +698,7 @@ private async Task VerifyProjectAsync(ProjectState newState, Project project, IV Diagnostic? firstDiagnostic = null; CodeFixProvider? effectiveCodeFixProvider = null; string? equivalenceKey = null; - foreach (var diagnostic in analyzerDiagnostics) + foreach (var (_, diagnostic) in analyzerDiagnostics) { var actions = new List<(CodeAction, CodeFixProvider)>(); @@ -819,7 +819,7 @@ private static async Task GetSourceTextFromDocumentAsync(Document do return await formatted.GetTextAsync(cancellationToken).ConfigureAwait(false); } - private static bool AreDiagnosticsDifferent(ImmutableArray analyzerDiagnostics, ImmutableArray previousDiagnostics) + private static bool AreDiagnosticsDifferent(ImmutableArray<(Project project, Diagnostic diagnostic)> analyzerDiagnostics, ImmutableArray<(Project project, Diagnostic diagnostic)> previousDiagnostics) { if (analyzerDiagnostics.Length != previousDiagnostics.Length) { @@ -828,8 +828,9 @@ private static bool AreDiagnosticsDifferent(ImmutableArray analyzerD for (var i = 0; i < analyzerDiagnostics.Length; i++) { - if ((analyzerDiagnostics[i].Id != previousDiagnostics[i].Id) - || (analyzerDiagnostics[i].Location.SourceSpan != previousDiagnostics[i].Location.SourceSpan)) + if ((analyzerDiagnostics[i].project.Id != previousDiagnostics[i].project.Id) + || (analyzerDiagnostics[i].diagnostic.Id != previousDiagnostics[i].diagnostic.Id) + || (analyzerDiagnostics[i].diagnostic.Location.SourceSpan != previousDiagnostics[i].diagnostic.Location.SourceSpan)) { return true; } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs index fb63c4122..07940362d 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs @@ -13,22 +13,22 @@ namespace Microsoft.CodeAnalysis.Testing { internal sealed class TestDiagnosticProvider : FixAllContext.DiagnosticProvider { - private readonly ImmutableArray _diagnostics; + private readonly ImmutableArray<(Project project, Diagnostic diagnostic)> _diagnostics; - private TestDiagnosticProvider(ImmutableArray diagnostics) + private TestDiagnosticProvider(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) { _diagnostics = diagnostics; } public override Task> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken) - => Task.FromResult>(_diagnostics); + => Task.FromResult>(_diagnostics.Where(diagnostic => diagnostic.project.Id == project.Id).Select(diagnostic => diagnostic.diagnostic)); public override Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken) - => Task.FromResult(_diagnostics.Where(i => i.Location.GetLineSpan().Path == document.Name)); + => Task.FromResult(_diagnostics.Where(i => i.diagnostic.Location.GetLineSpan().Path == document.Name).Where(diagnostic => diagnostic.project.Id == document.Project.Id).Select(diagnostic => diagnostic.diagnostic)); public override Task> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken) - => Task.FromResult(_diagnostics.Where(i => !i.Location.IsInSource)); + => Task.FromResult(_diagnostics.Where(i => !i.diagnostic.Location.IsInSource).Where(diagnostic => diagnostic.project.Id == project.Id).Select(diagnostic => diagnostic.diagnostic)); - internal static TestDiagnosticProvider Create(ImmutableArray diagnostics) => new TestDiagnosticProvider(diagnostics); + internal static TestDiagnosticProvider Create(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) => new TestDiagnosticProvider(diagnostics); } }