diff --git a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs index d4bff48bc7..3d73c35489 100644 --- a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs +++ b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs @@ -59,22 +59,22 @@ private static async Task ConvertCodeAsync(Document document, Cancella // Always use the latest updatedRoot as input for the next transformation var updatedRoot = UpdateInitializeDispose(compilation, root); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); updatedRoot = UpdateClassAttributes(compilation, updatedRoot); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); updatedRoot = RemoveInterfacesAndBaseClasses(compilation, updatedRoot); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); updatedRoot = ConvertTheoryData(compilation, updatedRoot); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); updatedRoot = ConvertTestOutputHelpers(ref compilation, ref syntaxTree, updatedRoot); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); updatedRoot = RemoveUsingDirectives(updatedRoot); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, updatedRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref updatedRoot); // Apply all changes in one step return document.WithSyntaxRoot(updatedRoot); @@ -104,7 +104,7 @@ private static SyntaxNode ConvertTestOutputHelpers(ref Compilation compilation, ) ); - UpdateSyntaxTrees(ref compilation, ref syntaxTree, currentRoot); + UpdateSyntaxTrees(ref compilation, ref syntaxTree, ref currentRoot); compilationValue = compilation; } @@ -494,10 +494,15 @@ public override SyntaxNode VisitAttributeList(AttributeListSyntax node) SyntaxFactory.IdentifierName("Obsolete")))], _ => [attr] }; - + newAttributes.AddRange(converted); } + if (node.Attributes.SequenceEqual(newAttributes)) + { + return node; + } + // Preserve original trivia instead of forcing elastic trivia return SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList(newAttributes)) .WithLeadingTrivia(node.GetLeadingTrivia()) @@ -718,9 +723,24 @@ public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) } } - private static void UpdateSyntaxTrees(ref Compilation compilation, ref SyntaxTree syntaxTree, SyntaxNode updatedRoot) + private static void UpdateSyntaxTrees(ref Compilation compilation, ref SyntaxTree syntaxTree, ref SyntaxNode updatedRoot) { - compilation = compilation.ReplaceSyntaxTree(syntaxTree, updatedRoot.SyntaxTree); - syntaxTree = updatedRoot.SyntaxTree; + var parseOptions = syntaxTree.Options; + var newSyntaxTree = updatedRoot.SyntaxTree; + + // If the parse options differ, re-parse the updatedRoot with the correct options + if (!Equals(newSyntaxTree.Options, parseOptions)) + { + newSyntaxTree = CSharpSyntaxTree.ParseText( + updatedRoot.ToFullString(), + (CSharpParseOptions)parseOptions, + syntaxTree.FilePath + ); + } + + compilation = compilation.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); + syntaxTree = newSyntaxTree; + + updatedRoot = newSyntaxTree.GetRoot(); } } diff --git a/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs b/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs index 0bc3652d72..4d6afe91c1 100644 --- a/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs +++ b/TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Polly.CircuitBreaker; using TUnit.Core; using TUnit.TestProject.Library; @@ -25,13 +26,18 @@ public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => CSharpAnalyzerVerifier.Diagnostic(descriptor); /// - public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, params DiagnosticResult[] expected) + public static Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, params DiagnosticResult[] expected) + { + return VerifyAnalyzerAsync(source, _ => { }, expected); + } + + /// + public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, Action configureTest, params DiagnosticResult[] expected) { var test = new Test { TestCode = source, - ReferenceAssemblies = ReferenceAssemblies.Net.Net90 - .AddPackages([new PackageIdentity("xunit.v3.extensibility.core", "2.0.0")]), + ReferenceAssemblies = ReferenceAssemblies.Net.Net90, TestState = { AdditionalReferences = @@ -44,6 +50,9 @@ public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, }; test.ExpectedDiagnostics.AddRange(expected); + + configureTest(test); + await test.RunAsync(CancellationToken.None); } } \ No newline at end of file diff --git a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs index 5f0706cd0d..368a73b58b 100644 --- a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs +++ b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs @@ -25,9 +25,16 @@ public static DiagnosticResult Diagnostic(string diagnosticId) public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) => CSharpCodeFixVerifier.Diagnostic(descriptor); + /// + public static Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, params DiagnosticResult[] expected) + { + return VerifyAnalyzerAsync(source, _ => { }, expected); + } + /// public static async Task VerifyAnalyzerAsync( [StringSyntax("c#")] string source, + Action configureTest, params DiagnosticResult[] expected ) { @@ -47,6 +54,9 @@ params DiagnosticResult[] expected }; test.ExpectedDiagnostics.AddRange(expected); + + configureTest(test); + await test.RunAsync(CancellationToken.None); } @@ -58,19 +68,32 @@ public static async Task VerifyCodeFixAsync([StringSyntax("c#")] string source, public static async Task VerifyCodeFixAsync([StringSyntax("c#")] string source, DiagnosticResult expected, [StringSyntax("c#")] string fixedSource) => await VerifyCodeFixAsync(source, [expected], fixedSource); + public static async Task VerifyCodeFixAsync([StringSyntax("c#")] string source, DiagnosticResult expected, [StringSyntax("c#")] string fixedSource, Action configureTest) + => await VerifyCodeFixAsync(source, [expected], fixedSource, configureTest); + /// - public static async Task VerifyCodeFixAsync( + public static Task VerifyCodeFixAsync( [StringSyntax("c#")] string source, IEnumerable expected, [StringSyntax("c#")] string fixedSource ) + { + return VerifyCodeFixAsync(source, expected, fixedSource, _ => { }); + } + + /// + public static async Task VerifyCodeFixAsync( + [StringSyntax("c#")] string source, + IEnumerable expected, + [StringSyntax("c#")] string fixedSource, + Action configureTest + ) { var test = new Test { TestCode = source.NormalizeLineEndings(), FixedCode = fixedSource.NormalizeLineEndings(), - ReferenceAssemblies = ReferenceAssemblies.Net.Net90 - .AddPackages([new PackageIdentity("xunit.v3.extensibility.core", "2.0.0")]), + ReferenceAssemblies = ReferenceAssemblies.Net.Net90, TestState = { AdditionalReferences = @@ -83,6 +106,9 @@ public static async Task VerifyCodeFixAsync( }; test.ExpectedDiagnostics.AddRange(expected); + + configureTest(test); + await test.RunAsync(CancellationToken.None); } } \ No newline at end of file diff --git a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs index 942d1889ba..bfaa0354e2 100644 --- a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs @@ -1,3 +1,5 @@ +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; using CodeFixer = TUnit.Analyzers.Tests.Verifiers.CSharpCodeFixVerifier; @@ -15,9 +17,7 @@ public async Task Test_Attribute_Flagged(string attributeName) await Verifier .VerifyAnalyzerAsync( $$""" - {|#0:using Xunit; - - public class MyClass + {|#0:public class MyClass { [{{attributeName}}] public void MyTest() @@ -25,10 +25,11 @@ public void MyTest() } }|} """, + ConfigureXUnitTest, Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0) ); } - + [Arguments("Fact", "Test")] [Arguments("Theory", "Test")] [Arguments("InlineData", "Arguments")] @@ -48,7 +49,6 @@ await CodeFixer .VerifyCodeFixAsync( $$""" {|#0:using TUnit.Core; - using Xunit; public class MyClass { @@ -69,10 +69,11 @@ public void MyTest() { } } - """ + """, + ConfigureXUnitTest ); } - + [Test] [Arguments("Fact")] [Arguments("Theory")] @@ -84,7 +85,6 @@ await CodeFixer .VerifyCodeFixAsync( $$""" {|#0:using TUnit.Core; - using Xunit; public class MyClass { @@ -105,7 +105,8 @@ public void MyTest() { } } - """ + """, + ConfigureXUnitTest ); } @@ -116,7 +117,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyType; @@ -154,10 +154,11 @@ public void MyTest() public class MyCollection { } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task Collection_Disable_Parallelism_Attributes_Can_Be_Fixed() { @@ -165,7 +166,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyType; @@ -203,10 +203,11 @@ public void MyTest() public class MyCollection { } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task Combined_Collection_Fixture_And_Disable_Parallelism_Attributes_Can_Be_Fixed() { @@ -214,7 +215,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyType; @@ -252,7 +252,8 @@ public void MyTest() public class MyCollection { } - """ + """, + ConfigureXUnitTest ); } @@ -266,7 +267,6 @@ await CodeFixer $$""" {|#0:using System; using TUnit.Core; - using Xunit; [assembly: {|#0:{{attribute}}|}] namespace MyNamespace; @@ -294,19 +294,18 @@ public void MyTest() { } } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task ClassFixture_Flagged() { await Verifier .VerifyAnalyzerAsync( """ - {|#0:using Xunit; - - public class MyType; + {|#0:public class MyType; public class MyClass : IClassFixture { @@ -316,19 +315,18 @@ public void MyTest() } }|} """, + ConfigureXUnitTest, Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0) ); } - + [Test] public async Task ClassFixture_Can_Be_Fixed() { await CodeFixer .VerifyCodeFixAsync( """ - {|#0:using Xunit; - - public class MyType; + {|#0:public class MyType; public class MyClass(MyType myType) : IClassFixture { @@ -349,10 +347,11 @@ public void MyTest() { } } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task Xunit_Directive_Flagged() { @@ -370,10 +369,11 @@ public void MyTest() } }|} """, + ConfigureXUnitTest, Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0) ); } - + [Test] public async Task Xunit_Directive_Can_Be_Removed() { @@ -402,10 +402,11 @@ public void MyTest() { } } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task Test_Initialize_Can_Be_Converted() { @@ -413,7 +414,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyClass : IAsyncLifetime { @@ -456,10 +456,11 @@ public Task DisposeAsync() return default; } } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task NonTest_Initialize_Can_Be_Converted() { @@ -467,7 +468,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyClass : IAsyncLifetime { @@ -498,18 +498,18 @@ public ValueTask DisposeAsync() return default; } } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task TheoryData_Is_Flagged() { - await CodeFixer + await Verifier .VerifyAnalyzerAsync( """ {|#0:using System; - using Xunit; public class MyClass { @@ -521,10 +521,11 @@ public class MyClass }; }|} """, + ConfigureXUnitTest, Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0) ); } - + [Test] public async Task TheoryData_Can_Be_Converted() { @@ -532,7 +533,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class MyClass { @@ -557,24 +557,24 @@ public class MyClass TimeSpan.FromMilliseconds(10) }; } - """ + """, + ConfigureXUnitTest ); } - + [Test] public async Task ITestOutputHelper_Is_Flagged() { - await CodeFixer + await Verifier .VerifyAnalyzerAsync( """ {|#0:using System; - using Xunit; public class UnitTest1(ITestOutputHelper testOutputHelper) { private ITestOutputHelper _testOutputHelper = testOutputHelper; public ITestOutputHelper TestOutputHelper { get; } = testOutputHelper; - + [Fact] public void Test1() { @@ -583,10 +583,11 @@ public void Test1() } }|} """, + ConfigureXUnitTest, Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0) ); } - + [Test] public async Task ITestOutputHelper_Can_Be_Converted() { @@ -594,7 +595,6 @@ await CodeFixer .VerifyCodeFixAsync( """ {|#0:using TUnit.Core; - using Xunit; public class UnitTest1(ITestOutputHelper testOutputHelper) { @@ -622,7 +622,33 @@ public void Test1() Console.WriteLine("Bar"); } } - """ + """, + ConfigureXUnitTest ); } -} \ No newline at end of file + + private static void ConfigureXUnitTest(Verifier.Test test) + { + var globalUsings = ("GlobalUsings.cs", SourceText.From("global using Xunit;")); + + test.TestState.Sources.Add(globalUsings); + + test.ReferenceAssemblies = test.ReferenceAssemblies.AddPackages([ + new PackageIdentity("xunit.v3.extensibility.core", "2.0.0") + ]); + } + + private static void ConfigureXUnitTest(CodeFixer.Test test) + { + var globalUsings = ("GlobalUsings.cs", SourceText.From("global using Xunit;")); + + test.TestState.Sources.Add(globalUsings); + test.FixedState.Sources.Add(globalUsings); + + test.ReferenceAssemblies = test.ReferenceAssemblies.AddPackages([ + new PackageIdentity("xunit.v3.extensibility.core", "2.0.0") + ]); + } +} + + diff --git a/TUnit.Analyzers/Migrators/XUnitMigrationAnalyzer.cs b/TUnit.Analyzers/Migrators/XUnitMigrationAnalyzer.cs index 2b49d1f4c1..084b7b7a64 100644 --- a/TUnit.Analyzers/Migrators/XUnitMigrationAnalyzer.cs +++ b/TUnit.Analyzers/Migrators/XUnitMigrationAnalyzer.cs @@ -39,10 +39,7 @@ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) if (symbol.AllInterfaces.Any(i => i.ContainingNamespace.Name.StartsWith("Xunit"))) { - context.ReportDiagnostic( - Diagnostic.Create(Rules.XunitMigration, context.Node.GetLocation()) - ); - + Flag(context); return; } @@ -69,10 +66,32 @@ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) if (usingDirectiveSyntax.Name is QualifiedNameSyntax { Left: IdentifierNameSyntax { Identifier.Text: "Xunit" } } or IdentifierNameSyntax { Identifier.Text: "Xunit" }) { - context.ReportDiagnostic(Diagnostic.Create(Rules.XunitMigration, context.Node.GetLocation())); + Flag(context); return; } } + + var namedTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + + if (namedTypeSymbol is null) + { + return; + } + + var members = namedTypeSymbol.GetMembers(); + + ITypeSymbol[] types = + [ + ..members.OfType().Where(x => x.Type.ContainingNamespace.Name.StartsWith("Xunit")).Select(x => x.Type), + ..members.OfType().Where(x => x.ReturnType.ContainingNamespace.Name.StartsWith("Xunit")).Select(x => x.ReturnType), + ..members.OfType().Where(x => x.Type.ContainingNamespace.Name.StartsWith("Xunit")).Select(x => x.Type), + ]; + + if (types.Any()) + { + Flag(context); + return; + } } } @@ -84,14 +103,16 @@ private bool AnalyzeAttributes(SyntaxNodeAnalysisContext context, ISymbol symbol if(@namespace == "Xunit") { - context.ReportDiagnostic( - Diagnostic.Create(Rules.XunitMigration, context.Node.GetLocation()) - ); - + Flag(context); return true; } } return false; } + + private static void Flag(SyntaxNodeAnalysisContext context) + { + context.ReportDiagnostic(Diagnostic.Create(Rules.XunitMigration, context.Node.GetLocation())); + } } \ No newline at end of file