diff --git a/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs b/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs index 4bcfe745de..1f6a8d0ee9 100644 --- a/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs @@ -1652,6 +1652,8 @@ private AttributeConversion ConvertTestAttributeFull(AttributeSyntax node) { // [Platform(Include = "Win")] -> [RunOn(OS.Windows)] // [Platform(Exclude = "Linux")] -> [ExcludeOn(OS.Linux)] + // Note: Unsupported platforms like "Mono", "Net", "NetCore" have no direct TUnit equivalent + // and will be left unconverted (returning null) if (node.ArgumentList?.Arguments.Count > 0) { foreach (var arg in node.ArgumentList.Arguments) @@ -1662,11 +1664,21 @@ private AttributeConversion ConvertTestAttributeFull(AttributeSyntax node) if (nameText == "Include") { var os = MapPlatformToOS(valueText); + if (os == null) + { + // Unsupported platform - don't convert + return (null, null); + } return ("RunOn", $"({os})"); } if (nameText == "Exclude") { var os = MapPlatformToOS(valueText); + if (os == null) + { + // Unsupported platform - don't convert + return (null, null); + } return ("ExcludeOn", $"({os})"); } } @@ -1674,28 +1686,38 @@ private AttributeConversion ConvertTestAttributeFull(AttributeSyntax node) return (null, null); } - private static string MapPlatformToOS(string platform) + private static string? MapPlatformToOS(string platform) { // Handle multiple platforms separated by comma: "Win,Linux" -> "OS.Windows | OS.Linux" if (platform.Contains(",")) { - var platforms = platform.Split(',') - .Select(p => MapSinglePlatformToOS(p.Trim())) - .ToArray(); - return string.Join(" | ", platforms); + var mappedPlatforms = new List(); + foreach (var p in platform.Split(',')) + { + var mapped = MapSinglePlatformToOS(p.Trim()); + if (mapped == null) + { + // If any platform is unsupported, return null + return null; + } + mappedPlatforms.Add(mapped); + } + return string.Join(" | ", mappedPlatforms); } return MapSinglePlatformToOS(platform); } - private static string MapSinglePlatformToOS(string platform) + private static string? MapSinglePlatformToOS(string platform) { return platform.ToLowerInvariant() switch { "win" or "windows" or "win32" or "win64" => "OS.Windows", "linux" or "unix" => "OS.Linux", - "macos" or "osx" or "macosx" => "OS.MacOS", - _ => $"OS.{platform}" + "macos" or "osx" or "macosx" => "OS.MacOs", + "browser" or "wasm" or "webassembly" => "OS.Browser", + // Unsupported platforms - return null to indicate no direct equivalent + _ => null }; } diff --git a/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs b/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs index 8cfc9bce56..91932fd75a 100644 --- a/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/AbstractTestClassWithDataSourcesAnalyzerTests.cs @@ -241,4 +241,60 @@ public void TestName(bool flag) { } .WithArguments("Tests") ); } + + [Test] + public async Task Warning_When_Multiple_Concrete_Classes_Exist_But_None_Have_InheritsTests() + { + // When there are multiple subclasses but none have [InheritsTests], warn + await Verifier + .VerifyAnalyzerAsync( + """ + using TUnit.Core; + + public class Tests1 : Tests { } + public class Tests2 : Tests { } + public class Tests3 : Tests { } + + public abstract class {|#0:Tests|} + { + [Test] + [Arguments(true)] + [Arguments(false)] + public void TestName(bool flag) { } + } + """, + + Verifier.Diagnostic(Rules.AbstractTestClassWithDataSources) + .WithLocation(0) + .WithArguments("Tests") + ); + } + + [Test] + public async Task No_Warning_When_At_Least_One_Concrete_Class_Has_InheritsTests() + { + // When at least one subclass has [InheritsTests], no warning + // (even if other subclasses don't have it) + await Verifier + .VerifyAnalyzerAsync( + """ + using TUnit.Core; + + public class Tests1 : Tests { } + + [InheritsTests] + public class Tests2 : Tests { } + + public class Tests3 : Tests { } + + public abstract class Tests + { + [Test] + [Arguments(true)] + [Arguments(false)] + public void TestName(bool flag) { } + } + """ + ); + } } diff --git a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs index 7124e5b1cd..6f5dcc06f9 100644 --- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs @@ -5294,6 +5294,172 @@ public async Task BooleanTest(bool value) ); } + [Test] + public async Task NUnit_Platform_MacOS_Converted_To_RunOn_With_Correct_Casing() + { + // Issue #4489: MacOS should be converted to OS.MacOs (not OS.MacOS) + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + public class MyClass + { + {|#0:[Test]|} + [Platform(Include = "MacOS")] + public void TestMethod() + { + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + + public class MyClass + { + [Test] + [RunOn(OS.MacOs)] + public void TestMethod() + { + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_Platform_OSX_Converted_To_RunOn_MacOs() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + public class MyClass + { + {|#0:[Test]|} + [Platform(Include = "OSX")] + public void TestMethod() + { + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + + public class MyClass + { + [Test] + [RunOn(OS.MacOs)] + public void TestMethod() + { + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_Platform_Unsupported_Mono_Not_Converted() + { + // Issue #4489: Unsupported platforms like Mono should NOT be converted + // because TUnit doesn't have an equivalent OS value + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + public class MyClass + { + {|#0:[Test]|} + [Platform(Exclude = "Mono")] + public void TestMethod() + { + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + + public class MyClass + { + [Test] + [Platform(Exclude = "Mono")] + public void TestMethod() + { + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_Platform_Unsupported_Net_Not_Converted() + { + // Issue #4489: Unsupported platforms like Net should NOT be converted + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + public class MyClass + { + {|#0:[Test]|} + [Platform(Exclude = "Net")] + public void TestMethod() + { + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + + public class MyClass + { + [Test] + [Platform(Exclude = "Net")] + public void TestMethod() + { + } + } + """, + ConfigureNUnitTest + ); + } + + [Test] + public async Task NUnit_Platform_Mixed_Supported_And_Unsupported_Not_Converted() + { + // Issue #4489: If any platform in a comma-separated list is unsupported, + // the whole attribute should not be converted + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + public class MyClass + { + {|#0:[Test]|} + [Platform(Include = "Win,Mono")] + public void TestMethod() + { + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + + public class MyClass + { + [Test] + [Platform(Include = "Win,Mono")] + public void TestMethod() + { + } + } + """, + ConfigureNUnitTest + ); + } + private static void ConfigureNUnitTest(Verifier.Test test) { test.TestState.AdditionalReferences.Add(typeof(NUnit.Framework.TestAttribute).Assembly); diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index ab94042e11..90ce04dc07 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -120,14 +120,22 @@ dotnet format analyzers --severity info --diagnostics TUMS0001 This command applies all available fixes for the `TUMS0001` diagnostic. You'll see output indicating which files were modified. -:::tip Multi-targeting Projects -If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), specify the latest framework to avoid issues: +:::warning Multi-targeting Projects +If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), you **must** specify a single target framework when running the code fixer. Multi-targeting can cause the code fixer to crash with the error `Changes must be within bounds of SourceText` due to a limitation in Roslyn's linked file handling. +**Option 1:** Specify a single framework via command line: ```bash dotnet format analyzers --severity info --diagnostics TUMS0001 --framework net10.0 ``` -Replace `net10.0` with your project's highest supported target framework. +**Option 2:** Temporarily modify your project file to single-target: +```xml + +net10.0 + +``` + +Run the code fixer, then restore multi-targeting afterward. Replace `net10.0` with your project's highest supported target framework. ::: **5. Remove the implicit usings workaround** diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index 641cbbe669..7545246f75 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -108,14 +108,22 @@ dotnet format analyzers --severity info --diagnostics TUNU0001 This command applies all available fixes for the `TUNU0001` diagnostic. You'll see output indicating which files were modified. -:::tip Multi-targeting Projects -If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), specify the latest framework to avoid issues: +:::warning Multi-targeting Projects +If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), you **must** specify a single target framework when running the code fixer. Multi-targeting can cause the code fixer to crash with the error `Changes must be within bounds of SourceText` due to a limitation in Roslyn's linked file handling. +**Option 1:** Specify a single framework via command line: ```bash dotnet format analyzers --severity info --diagnostics TUNU0001 --framework net10.0 ``` -Replace `net10.0` with your project's highest supported target framework. +**Option 2:** Temporarily modify your project file to single-target: +```xml + +net10.0 + +``` + +Run the code fixer, then restore multi-targeting afterward. Replace `net10.0` with your project's highest supported target framework. ::: **5. Remove the implicit usings workaround** diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index 311f1823cd..866360e0c0 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -101,14 +101,22 @@ dotnet format analyzers --severity info --diagnostics TUXU0001 This command applies all available fixes for the `TUXU0001` diagnostic. You'll see output indicating which files were modified. -:::tip Multi-targeting Projects -If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), specify the latest framework to avoid issues: +:::warning Multi-targeting Projects +If your project targets multiple .NET versions (e.g., `net8.0;net9.0;net10.0`), you **must** specify a single target framework when running the code fixer. Multi-targeting can cause the code fixer to crash with the error `Changes must be within bounds of SourceText` due to a limitation in Roslyn's linked file handling. +**Option 1:** Specify a single framework via command line: ```bash dotnet format analyzers --severity info --diagnostics TUXU0001 --framework net10.0 ``` -Replace `net10.0` with your project's highest supported target framework. +**Option 2:** Temporarily modify your project file to single-target: +```xml + +net10.0 + +``` + +Run the code fixer, then restore multi-targeting afterward. Replace `net10.0` with your project's highest supported target framework. ::: **5. Remove the implicit usings workaround**