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));
+ }
+ }
+}