From 4431ec120fcd1ce9f212d5bc5db8bfa2a914a5f7 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 21 Jun 2021 16:31:23 -0700 Subject: [PATCH] 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 | 69 ++++ 13 files changed, 702 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 a0188b0fa0..bdaa950b9e 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 4593954579..73f93a6270 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 d108fdc130..91f8237e90 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 2ab84d52f8..14d5b1ccb0 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 c207545c87..de2c8c122a 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 6e09256b2a..1552a9842b 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 3b2e0def15..80ee3581f0 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 43704cacfb..5b38386b76 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 0000000000..35059a454a --- /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 b47c232dcb..03780afb8a 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 614d810052..184f5e0fec 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 0000000000..281dd7808a --- /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 0000000000..ae035b53e7 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs @@ -0,0 +1,69 @@ +// 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) + { + ////Assert.True(originalLeadingTrivia[0].IsKind(CSharp.SyntaxKind.WhitespaceTrivia)); + ////newLeadingTrivia = CSharp.SyntaxFactory.TriviaList(CSharp.SyntaxFactory.Whitespace(" " + originalLeadingTrivia[0].ToFullString())); + newLeadingTrivia = CSharp.SyntaxFactory.TriviaList(CSharp.SyntaxFactory.Space); + } + else + { + ////Assert.True(originalLeadingTrivia[0].IsKind(VisualBasic.SyntaxKind.WhitespaceTrivia)); + ////newLeadingTrivia = VisualBasic.SyntaxFactory.TriviaList(VisualBasic.SyntaxFactory.Whitespace(" " + originalLeadingTrivia[0].ToFullString())); + 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)); + } + } +}