diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx index 569baca6de..b81c37edd2 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx @@ -207,4 +207,7 @@ Use 'TestContext.CancellationToken' instead + + Use '[OSCondition]' attribute + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/UseOSConditionAttributeInsteadOfRuntimeCheckFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/UseOSConditionAttributeInsteadOfRuntimeCheckFixer.cs new file mode 100644 index 0000000000..7aec308e44 --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/UseOSConditionAttributeInsteadOfRuntimeCheckFixer.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Composition; + +using Analyzer.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +using MSTest.Analyzers.Helpers; + +namespace MSTest.Analyzers; + +/// +/// Code fixer for . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseOSConditionAttributeInsteadOfRuntimeCheckFixer))] +[Shared] +public sealed class UseOSConditionAttributeInsteadOfRuntimeCheckFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DiagnosticIds.UseOSConditionAttributeInsteadOfRuntimeCheckRuleId); + + /// + public override FixAllProvider GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + Diagnostic diagnostic = context.Diagnostics[0]; + + string? isNegatedStr = diagnostic.Properties[UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer.IsNegatedKey]; + string? osPlatform = diagnostic.Properties[UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer.OSPlatformKey]; + + if (isNegatedStr is null || osPlatform is null || !bool.TryParse(isNegatedStr, out bool isNegated)) + { + return; + } + + SyntaxNode diagnosticNode = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + // Find the containing method + MethodDeclarationSyntax? methodDeclaration = diagnosticNode.FirstAncestorOrSelf(); + if (methodDeclaration is null) + { + return; + } + + // Find the if statement to remove + IfStatementSyntax? ifStatement = diagnosticNode.FirstAncestorOrSelf(); + if (ifStatement is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: CodeFixResources.UseOSConditionAttributeInsteadOfRuntimeCheckFix, + createChangedDocument: ct => AddOSConditionAttributeAsync(context.Document, methodDeclaration, ifStatement, osPlatform, isNegated, ct), + equivalenceKey: nameof(UseOSConditionAttributeInsteadOfRuntimeCheckFixer)), + diagnostic); + } + + private static async Task AddOSConditionAttributeAsync( + Document document, + MethodDeclarationSyntax methodDeclaration, + IfStatementSyntax ifStatement, + string osPlatform, + bool isNegated, + CancellationToken cancellationToken) + { + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + + string? operatingSystem = MapOSPlatformToOperatingSystem(osPlatform); + if (operatingSystem is null) + { + return document; + } + + MethodDeclarationSyntax? modifiedMethod = RemoveIfStatementFromMethod(methodDeclaration, ifStatement); + if (modifiedMethod is null) + { + return document; + } + + AttributeSyntax? existingAttribute = FindExistingOSConditionAttribute(methodDeclaration); + MethodDeclarationSyntax newMethod = existingAttribute is not null + ? UpdateMethodWithCombinedAttribute(modifiedMethod, existingAttribute, operatingSystem, isNegated) + : AddNewAttributeToMethod(modifiedMethod, operatingSystem, isNegated); + + editor.ReplaceNode(methodDeclaration, newMethod); + return editor.GetChangedDocument(); + } + + private static MethodDeclarationSyntax? RemoveIfStatementFromMethod( + MethodDeclarationSyntax methodDeclaration, + IfStatementSyntax ifStatement) + { + MethodDeclarationSyntax trackedMethod = methodDeclaration.TrackNodes(ifStatement); + IfStatementSyntax? trackedIfStatement = trackedMethod.GetCurrentNode(ifStatement); + + return trackedIfStatement is not null + ? trackedMethod.RemoveNode(trackedIfStatement, SyntaxRemoveOptions.KeepNoTrivia) + : null; + } + + private static AttributeSyntax? FindExistingOSConditionAttribute(MethodDeclarationSyntax methodDeclaration) + => methodDeclaration.AttributeLists + .SelectMany(al => al.Attributes) + .FirstOrDefault(a => a.Name.ToString() is "OSCondition" or "OSConditionAttribute"); + + private static MethodDeclarationSyntax UpdateMethodWithCombinedAttribute( + MethodDeclarationSyntax method, + AttributeSyntax existingAttribute, + string operatingSystem, + bool isNegated) + { + ExistingAttributeInfo attributeInfo = ParseExistingAttribute(existingAttribute); + + // Only combine if the condition modes match + if (CanCombineAttributes(attributeInfo.IsIncludeMode, isNegated)) + { + string combinedOSValue = CombineOSValues(attributeInfo.OSValue, operatingSystem); + AttributeSyntax newAttribute = CreateCombinedAttribute(combinedOSValue, isNegated); + return ReplaceExistingAttribute(method, newAttribute); + } + + // Different condition modes - add as separate attribute + // (This shouldn't happen in practice since OSCondition doesn't allow multiple attributes) + return AddNewAttributeToMethod(method, operatingSystem, isNegated); + } + + private static ExistingAttributeInfo ParseExistingAttribute(AttributeSyntax attribute) + { + if (attribute.ArgumentList is null) + { + return new ExistingAttributeInfo(IsIncludeMode: true, OSValue: null); + } + + SeparatedSyntaxList args = attribute.ArgumentList.Arguments; + + return args.Count switch + { + // [OSCondition(OperatingSystems.Linux)] - Include mode + 1 => new ExistingAttributeInfo( + IsIncludeMode: true, + OSValue: args[0].Expression.ToString()), + + // [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + 2 => new ExistingAttributeInfo( + IsIncludeMode: !args[0].Expression.ToString().Contains("Exclude"), + OSValue: args[1].Expression.ToString()), + + _ => new ExistingAttributeInfo(IsIncludeMode: true, OSValue: null), + }; + } + + private static bool CanCombineAttributes(bool existingIsIncludeMode, bool isNegated) + => (isNegated && existingIsIncludeMode) || (!isNegated && !existingIsIncludeMode); + + private static string CombineOSValues(string? existingOSValue, string newOperatingSystem) + => existingOSValue is not null + ? $"{existingOSValue} | OperatingSystems.{newOperatingSystem}" + : $"OperatingSystems.{newOperatingSystem}"; + + private static AttributeSyntax CreateCombinedAttribute(string osValue, bool isNegated) + { + if (isNegated) + { + // Include mode (default) + return SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("OSCondition"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression(osValue))))); + } + + // Exclude mode + return SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("OSCondition"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression("ConditionMode.Exclude")), + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression(osValue)), + }))); + } + + private static MethodDeclarationSyntax ReplaceExistingAttribute( + MethodDeclarationSyntax method, + AttributeSyntax newAttribute) + { + AttributeListSyntax? oldAttributeList = method.AttributeLists + .FirstOrDefault(al => al.Attributes.Any(a => a.Name.ToString() is "OSCondition" or "OSConditionAttribute")); + + if (oldAttributeList is null) + { + return method; + } + + AttributeListSyntax newAttributeList = SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList(newAttribute)) + .WithTrailingTrivia(oldAttributeList.GetTrailingTrivia()); + + return method.ReplaceNode(oldAttributeList, newAttributeList); + } + + private static MethodDeclarationSyntax AddNewAttributeToMethod( + MethodDeclarationSyntax method, + string operatingSystem, + bool isNegated) + { + AttributeListSyntax newAttributeList = CreateAttributeList(operatingSystem, isNegated); + return method.AddAttributeLists(newAttributeList); + } + + private static AttributeListSyntax CreateAttributeList(string operatingSystem, bool isNegated) + { + AttributeSyntax osConditionAttribute; + if (isNegated) + { + // Include mode is the default, so we only need to specify the operating system + osConditionAttribute = SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("OSCondition"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression($"OperatingSystems.{operatingSystem}"))))); + } + else + { + // Exclude mode must be explicitly specified + osConditionAttribute = SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("OSCondition"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression("ConditionMode.Exclude")), + SyntaxFactory.AttributeArgument( + SyntaxFactory.ParseExpression($"OperatingSystems.{operatingSystem}")), + }))); + } + + return SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList(osConditionAttribute)); + } + + private static string? MapOSPlatformToOperatingSystem(string osPlatform) + => osPlatform.ToUpperInvariant() switch + { + "WINDOWS" => "Windows", + "LINUX" => "Linux", + "OSX" => "OSX", + "FREEBSD" => "FreeBSD", + _ => null, + }; + + private readonly record struct ExistingAttributeInfo(bool IsIncludeMode, string? OSValue); +} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf index 8f9a0a8393..1ea1f670e7 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf @@ -151,6 +151,11 @@ Use '{0}' Použít {0} + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf index 374b6d3bde..313475fb06 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf @@ -151,6 +151,11 @@ Use '{0}' "{0}" verwenden + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf index 66742db404..e97058f5bd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf @@ -151,6 +151,11 @@ Use '{0}' Usar "{0}" + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf index ec66aaf4da..30796a6160 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf @@ -151,6 +151,11 @@ Use '{0}' Utiliser « {0} » + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf index fad6511ba2..cb6e35c416 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf @@ -151,6 +151,11 @@ Use '{0}' Usa '{0}' + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf index 422dd62c24..290bbc03e4 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf @@ -151,6 +151,11 @@ Use '{0}' '{0}' を使用します + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf index 2bd87cf1af..48ec68740c 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf @@ -151,6 +151,11 @@ Use '{0}' '{0}' 사용 + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf index 3ce2f0eda3..efda0db4e6 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf @@ -151,6 +151,11 @@ Use '{0}' Użyj „{0}” + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf index 3e7f19fb87..64e1d6eb05 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf @@ -151,6 +151,11 @@ Use '{0}' Usar '{0}' + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf index e442c31ee5..e581fc3f97 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf @@ -151,6 +151,11 @@ Use '{0}' Использовать "{0}" + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf index 28c764a1ea..7683df17c0 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf @@ -151,6 +151,11 @@ Use '{0}' '{0}' kullan + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf index 6563ec971c..1f105c3bb0 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf @@ -151,6 +151,11 @@ Use '{0}' 使用“{0}” + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf index bca5bd3526..f3ffce812c 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf @@ -151,6 +151,11 @@ Use '{0}' 使用 '{0}' + + + Use '[OSCondition]' attribute + Use '[OSCondition]' attribute + diff --git a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md index db7d4e4da2..cfc72a4c6c 100644 --- a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md @@ -6,3 +6,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- MSTEST0058 | Usage | Info | AvoidAssertsInCatchBlocksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0058) +MSTEST0059 | Usage | Info | UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0059) diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs index 15b809366e..d62d5c8603 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs @@ -63,4 +63,5 @@ internal static class DiagnosticIds public const string TestMethodAttributeShouldSetDisplayNameCorrectlyRuleId = "MSTEST0056"; public const string TestMethodAttributeShouldPropagateSourceInformationRuleId = "MSTEST0057"; public const string AvoidAssertsInCatchBlocksRuleId = "MSTEST0058"; + public const string UseOSConditionAttributeInsteadOfRuntimeCheckRuleId = "MSTEST0059"; } diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs index f2ed45a0f9..9f0729cb8b 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs @@ -40,13 +40,16 @@ internal static class WellKnownTypeNames public const string MicrosoftVisualStudioTestToolsUnitTestingTestPropertyAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingTimeoutAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TimeoutAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingWorkItemAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.WorkItemAttribute"; + public const string MicrosoftVisualStudioTestToolsUnitTestingOSConditionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.OSConditionAttribute"; public const string System = "System"; + public const string SystemRuntimeInteropServicesRuntimeInformation = "System.Runtime.InteropServices.RuntimeInformation"; public const string SystemCollectionsGenericIEnumerable1 = "System.Collections.Generic.IEnumerable`1"; public const string SystemDescriptionAttribute = "System.ComponentModel.DescriptionAttribute"; public const string SystemFunc1 = "System.Func`1"; public const string SystemIAsyncDisposable = "System.IAsyncDisposable"; public const string SystemIDisposable = "System.IDisposable"; + public const string SystemOperatingSystem = "System.OperatingSystem"; public const string SystemReflectionMethodInfo = "System.Reflection.MethodInfo"; public const string SystemRuntimeCompilerServicesCallerFilePathAttribute = "System.Runtime.CompilerServices.CallerFilePathAttribute"; public const string SystemRuntimeCompilerServicesCallerLineNumberAttribute = "System.Runtime.CompilerServices.CallerLineNumberAttribute"; diff --git a/src/Analyzers/MSTest.Analyzers/Resources.resx b/src/Analyzers/MSTest.Analyzers/Resources.resx index a8d5123404..0a2fbfb41c 100644 --- a/src/Analyzers/MSTest.Analyzers/Resources.resx +++ b/src/Analyzers/MSTest.Analyzers/Resources.resx @@ -699,4 +699,13 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer.cs new file mode 100644 index 0000000000..f6c38c7a89 --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer.cs @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Analyzer.Utilities.Extensions; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +using MSTest.Analyzers.Helpers; +using MSTest.Analyzers.RoslynAnalyzerHelpers; + +namespace MSTest.Analyzers; + +/// +/// MSTEST0059: Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive'. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public sealed class UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer : DiagnosticAnalyzer +{ + internal const string IsNegatedKey = nameof(IsNegatedKey); + internal const string OSPlatformKey = nameof(OSPlatformKey); + + private static readonly LocalizableResourceString Title = new(nameof(Resources.UseOSConditionAttributeInsteadOfRuntimeCheckTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.UseOSConditionAttributeInsteadOfRuntimeCheckMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableResourceString Description = new(nameof(Resources.UseOSConditionAttributeInsteadOfRuntimeCheckDescription), Resources.ResourceManager, typeof(Resources)); + + internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( + DiagnosticIds.UseOSConditionAttributeInsteadOfRuntimeCheckRuleId, + Title, + MessageFormat, + Description, + Category.Usage, + DiagnosticSeverity.Info, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(context => + { + if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute, out INamedTypeSymbol? testMethodAttributeSymbol) || + !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssert, out INamedTypeSymbol? assertSymbol)) + { + return; + } + + // Try to get RuntimeInformation.IsOSPlatform method + IMethodSymbol? isOSPlatformMethod = null; + if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesRuntimeInformation, out INamedTypeSymbol? runtimeInformationSymbol)) + { + isOSPlatformMethod = runtimeInformationSymbol.GetMembers("IsOSPlatform") + .OfType() + .FirstOrDefault(m => m.Parameters.Length == 1); + } + + // Try to get OperatingSystem type for IsWindows, IsLinux, etc. + context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemOperatingSystem, out INamedTypeSymbol? operatingSystemSymbol); + + // We need at least one of the two types + if (isOSPlatformMethod is null && operatingSystemSymbol is null) + { + return; + } + + context.RegisterOperationBlockStartAction(blockContext => + { + if (blockContext.OwningSymbol is not IMethodSymbol methodSymbol) + { + return; + } + + // Check if the method is a test method + bool isTestMethod = methodSymbol.GetAttributes().Any(attr => + attr.AttributeClass is not null && + attr.AttributeClass.Inherits(testMethodAttributeSymbol)); + + if (!isTestMethod) + { + return; + } + + IBlockOperation? methodBody = null; + blockContext.RegisterOperationAction( + operationContext => + { + // Capture the method body block operation + if (methodBody is null && operationContext.Operation is IBlockOperation block && block.Parent is IMethodBodyOperation) + { + methodBody = block; + } + }, + OperationKind.Block); + + blockContext.RegisterOperationAction( + operationContext => AnalyzeIfStatement(operationContext, isOSPlatformMethod, operatingSystemSymbol, assertSymbol, methodBody), + OperationKind.Conditional); + }); + }); + } + + private static void AnalyzeIfStatement(OperationAnalysisContext context, IMethodSymbol? isOSPlatformMethod, INamedTypeSymbol? operatingSystemSymbol, INamedTypeSymbol assertSymbol, IBlockOperation? methodBody) + { + var conditionalOperation = (IConditionalOperation)context.Operation; + + // Only analyze if statements (not ternary expressions) + if (conditionalOperation.WhenFalse is not null and not IBlockOperation { Operations.Length: 0 }) + { + // Has an else branch with content - more complex scenario, skip for now + return; + } + + // Only flag if statements that appear at the very beginning of the method body + // This ensures we don't flag if statements that come after other code + if (methodBody is not null && methodBody.Operations.Length > 0) + { + IOperation firstOperation = methodBody.Operations[0]; + if (firstOperation != conditionalOperation) + { + return; + } + } + + // Check if the condition is a RuntimeInformation.IsOSPlatform call or OperatingSystem.Is* call (or negation of it) + if (!TryGetOSPlatformFromCondition(conditionalOperation.Condition, isOSPlatformMethod, operatingSystemSymbol, out bool isNegated, out string? osPlatform)) + { + return; + } + + // Check if the body contains only early return or Assert.Inconclusive as the first statement + if (!IsEarlyReturnOrAssertInconclusive(conditionalOperation.WhenTrue, assertSymbol)) + { + return; + } + + // Report diagnostic + ImmutableDictionary.Builder properties = ImmutableDictionary.CreateBuilder(); + properties.Add(IsNegatedKey, isNegated.ToString()); + properties.Add(OSPlatformKey, osPlatform); + + context.ReportDiagnostic(conditionalOperation.CreateDiagnostic( + Rule, + properties: properties.ToImmutable())); + } + + private static bool TryGetOSPlatformFromCondition(IOperation condition, IMethodSymbol? isOSPlatformMethod, INamedTypeSymbol? operatingSystemSymbol, out bool isNegated, out string? osPlatform) + { + isNegated = false; + osPlatform = null; + + IOperation actualCondition = condition; + + // Handle negation: !RuntimeInformation.IsOSPlatform(...) or !OperatingSystem.IsWindows() + if (actualCondition is IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } unaryOp) + { + isNegated = true; + actualCondition = unaryOp.Operand; + } + + // Walk down any conversions + actualCondition = actualCondition.WalkDownConversion(); + + if (actualCondition is not IInvocationOperation invocation) + { + return false; + } + + // Check for RuntimeInformation.IsOSPlatform + if (isOSPlatformMethod is not null && + SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, isOSPlatformMethod)) + { + return TryGetOSPlatformFromIsOSPlatformCall(invocation, out osPlatform); + } + + // Check for OperatingSystem.Is* methods + return operatingSystemSymbol is not null && + SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, operatingSystemSymbol) && + TryGetOSPlatformFromOperatingSystemCall(invocation, out osPlatform); + } + + private static bool TryGetOSPlatformFromIsOSPlatformCall(IInvocationOperation invocation, out string? osPlatform) + { + osPlatform = null; + + // Get the OS platform from the argument + if (invocation.Arguments.Length != 1) + { + return false; + } + + IOperation argumentValue = invocation.Arguments[0].Value.WalkDownConversion(); + + // The argument is typically OSPlatform.Windows, OSPlatform.Linux, etc. + if (argumentValue is IPropertyReferenceOperation propertyRef) + { + osPlatform = propertyRef.Property.Name; + return true; + } + + // Could also be OSPlatform.Create("...") call + if (argumentValue is IInvocationOperation createInvocation && + createInvocation.TargetMethod.Name == "Create" && + createInvocation.Arguments.Length == 1 && + createInvocation.Arguments[0].Value.ConstantValue is { HasValue: true, Value: string platformName }) + { + osPlatform = platformName; + return true; + } + + return false; + } + + private static bool TryGetOSPlatformFromOperatingSystemCall(IInvocationOperation invocation, out string? osPlatform) + { + osPlatform = null; + + // Map OperatingSystem.Is* methods to platform names + string methodName = invocation.TargetMethod.Name; + osPlatform = methodName switch + { + "IsWindows" => "Windows", + "IsLinux" => "Linux", + "IsMacOS" => "OSX", + "IsFreeBSD" => "FreeBSD", + "IsAndroid" => "Android", + "IsIOS" => "iOS", + "IsTvOS" => "tvOS", + "IsWatchOS" => "watchOS", + "IsBrowser" => "Browser", + "IsWasi" => "Wasi", + "IsMacCatalyst" => "MacCatalyst", + _ => null, + }; + + return osPlatform is not null; + } + + private static bool IsEarlyReturnOrAssertInconclusive(IOperation? whenTrue, INamedTypeSymbol assertSymbol) + { + if (whenTrue is null) + { + return false; + } + + // If it's a block, check only the first operation + if (whenTrue is IBlockOperation blockOperation) + { + if (blockOperation.Operations.Length == 0) + { + return false; + } + + // Only check the first operation - must be return or Assert.Inconclusive + return IsReturnOrAssertInconclusive(blockOperation.Operations[0], assertSymbol); + } + + // Single statement (not in a block) + return IsReturnOrAssertInconclusive(whenTrue, assertSymbol); + } + + private static bool IsReturnOrAssertInconclusive(IOperation operation, INamedTypeSymbol assertSymbol) + { + // Check for return statement + if (operation is IReturnOperation) + { + return true; + } + + // Check for Assert.Inconclusive call + if (operation is IExpressionStatementOperation { Operation: IInvocationOperation invocation }) + { + if (SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, assertSymbol) && + invocation.TargetMethod.Name == "Inconclusive") + { + return true; + } + } + + return false; + } +} diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf index 1a543104f9..d235eae852 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf @@ -1013,6 +1013,21 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Používání kontrolních výrazů v blocích catch je problematické, protože test projde, i když se nevyvolá žádná výjimka a blok catch se nikdy nespustí. K ověření, že je vyvolána výjimka, použijte metody Assert.Throws, Assert.ThrowsExactly, Assert.ThrowsAsync nebo Assert.ThrowsExactlyAsync a poté proveďte další kontrolní výrazy nad zachycenou výjimkou bez použití bloku try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf index 99cde1ab58..ddef83b677 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf @@ -1014,6 +1014,21 @@ Der Typ, der diese Methoden deklariert, sollte auch die folgenden Regeln beachte Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Die Verwendung von Asserts in Catch-Blöcken ist problematisch, da der Test auch dann erfolgreich ist, wenn keine Ausnahme ausgelöst wird und der Catch-Block nie ausgeführt wird. Verwenden Sie „Assert.Throws“, „Assert.ThrowsExactly“, „Assert.ThrowsAsync“ oder „Assert.ThrowsExactlyAsync“, um zu überprüfen, ob eine Ausnahme ausgelöst wird, und erstellen Sie dann zusätzliche Assertionen für die abgefangene Ausnahme, ohne den „try-catch“-Block zu verwenden. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf index 0213f7dc26..9baa32207a 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf @@ -1013,6 +1013,21 @@ El tipo que declara estos métodos también debe respetar las reglas siguientes: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. El uso de aserciones en bloques catch es problemático porque la prueba se superará incluso aunque no se produzca ninguna excepción y el bloque catch nunca se ejecuta. Use "Assert.Throws", "Assert.ThrowsExactly", "Assert.ThrowsAsync" o "Assert.ThrowsExactlyAsync" para comprobar que se produzca una excepción y, a continuación, realice aserciones adicionales en la excepción detectada sin usar el bloque try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf index a7d3092250..c98f1bd8d4 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf @@ -1013,6 +1013,21 @@ Le type doit être une classe Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. L’utilisation d’assertions dans les blocs catch pose problème, car le test réussit même si aucune exception n’est levée et que le bloc catch n’est jamais exécuté. Utilisez « Assert.Throws », « Assert.ThrowsExactly », « Assert.ThrowsAsync » ou « Assert.ThrowsExactlyAsync » pour vérifier qu’une exception est levée, puis effectuez des assertions supplémentaires sur l’exception capturée sans utiliser le bloc try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf index 485a0cb6f0..7cd3e4ecac 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf @@ -1013,6 +1013,21 @@ Anche il tipo che dichiara questi metodi deve rispettare le regole seguenti: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. L'uso di asserzioni nei blocchi catch è problematico perché il test risulta superato anche se non viene generata alcuna eccezione e il blocco catch non viene mai eseguito. Utilizzare 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' o 'Assert.ThrowsExactlyAsync' per verificare che venga generata un'eccezione, quindi effettuare ulteriori asserzioni sull'eccezione rilevata senza usare il blocco try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf index fe8619a141..26154b3258 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf @@ -1013,6 +1013,21 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. catch ブロックでアサートを使用すると、例外がスローされず、catch ブロックが実行されなくてもテストが成功するため、問題があります。'Assert.Throws'、'Assert.ThrowsExactly'、'Assert.ThrowsAsync'、または 'Assert.ThrowsExactlyAsync' を使用して例外がスローされたことを確認し、try-catch ブロックを使用せずにキャッチされた例外に対して追加のアサートを実行します。 + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf index 2588630282..27d86cf316 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf @@ -1013,6 +1013,21 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. 예외가 발생하지 않고 catch 블록이 실행되지 않더라도 테스트가 통과하기 때문에 catch 블록에서 어설션을 사용하는 것은 문제가 됩니다. 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' 또는 'Assert.ThrowsExactlyAsync'를 사용하여 예외가 발생했는지 확인한 다음 try-catch 블록을 사용하지 않고 catch된 예외에 대해 추가 어설션을 만듭니다. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf index 3ed90ee583..53c5459e8c 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf @@ -1013,6 +1013,21 @@ Typ deklarujący te metody powinien również przestrzegać następujących regu Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Używanie asercji w blokach catch jest problematyczne, ponieważ test zakończy się powodzeniem, nawet jeśli nie zostanie zgłoszony żaden wyjątek i blok catch nigdy nie zostanie wykonany. Użyj instrukcji „Assert.Throws”, „Assert.ThrowsExactly”, „Assert.ThrowsAsync” lub „Assert.ThrowsExactlyAsync”, aby sprawdzić, czy zgłoszono wyjątek, a następnie wykonaj dodatkowe asercje dla przechwyconego wyjątku bez użycia bloku try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf index 6faf084979..0bcd88c407 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf @@ -1013,6 +1013,21 @@ O tipo que declara esses métodos também deve respeitar as seguintes regras: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Usar assertivas em blocos catch é problemático porque o teste será aprovado mesmo que nenhuma exceção seja lançada e o bloco catch nunca seja executado. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' ou 'Assert.ThrowsExactlyAsync' para verificar se uma exceção foi lançada e, em seguida, faça asserções adicionais sobre a exceção capturada sem usar o bloco try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf index d750be5bc5..b135f8865b 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf @@ -1025,6 +1025,21 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Использование утверждений в блоках catch является проблематичным, так как тест будет пройден, даже если не возникает исключение, а блок catch не выполняется. Используйте "Assert.Throws", "Assert.ThrowsExactly", "Assert.ThrowsAsync" или "Assert.ThrowsExactlyAsync" для проверки возникновения исключения, а затем выполните дополнительные утверждения перехваченного исключения без применения блока try-catch. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf index 3a0d151f19..501f5e2c6c 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf @@ -1015,6 +1015,21 @@ Bu yöntemleri bildiren tipin ayrıca aşağıdaki kurallara uyması gerekir: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. Catch bloklarında assert kullanmak sorunludur, çünkü özel durum atılmasa ve catch bloğu hiç çalıştırılmasa bile test geçecektir. Bir özel durumun atıldığını doğrulamak için ‘Assert.Throws’, ‘Assert.ThrowsExactly’, ‘Assert.ThrowsAsync’ veya ‘Assert.ThrowsExactlyAsync’ kullanın ve ardından try-catch bloğu kullanmadan yakalanan özel durum üzerinde ek doğrulamalar yapın. + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf index 2b56156c7f..dc3d4585d8 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf @@ -1013,6 +1013,21 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. 在 catch 块中使用断言是有问题的,因为即使没有引发异常且 catch 块从未执行,测试也会通过。请使用 "Assert.Throws"、"Assert.ThrowsExactly"、"Assert.ThrowsAsync" 或 "Assert.ThrowsExactlyAsync" 来验证是否引发了异常,然后对捕获的异常进行额外断言,而无需使用 try-catch 块。 + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf index caf46d3a24..e8fb0877ec 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf @@ -1013,6 +1013,21 @@ The type declaring these methods should also respect the following rules: Using asserts in catch blocks is problematic because the test will pass even if no exception is thrown and the catch block is never executed. Use 'Assert.Throws', 'Assert.ThrowsExactly', 'Assert.ThrowsAsync' or 'Assert.ThrowsExactlyAsync' to verify that an exception is thrown, and then make additional assertions on the caught exception without using the try-catch block. 在 Catch 區塊中使用斷言會有問題,因為即使沒有拋出例外且 Catch 區塊從未執行,測試仍會通過。請使用 'Assert.Throws'、'Assert.ThrowsExactly'、'Assert.ThrowsAsync' 或 'Assert.ThrowsExactlyAsync' 來驗證是否有拋出例外,然後在不使用 try-catch 區塊的情況下,對捕捉到的例外進行進一步判斷提示。 + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive' + + + + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + Test methods that use 'RuntimeInformation.IsOSPlatform' with early return or 'Assert.Inconclusive' should use the '[OSCondition]' attribute instead. This attribute provides a more declarative and discoverable way to specify OS-specific test requirements. + diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzerTests.cs new file mode 100644 index 0000000000..c071cee854 --- /dev/null +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzerTests.cs @@ -0,0 +1,766 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< + MSTest.Analyzers.UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer, + MSTest.Analyzers.UseOSConditionAttributeInsteadOfRuntimeCheckFixer>; + +namespace MSTest.Analyzers.Test; + +[TestClass] +public sealed class UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzerTests +{ + [TestMethod] + public async Task WhenNoRuntimeCheckUsed_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + // No RuntimeInformation.IsOSPlatform check + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithEarlyReturn_NotNegated_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithEarlyReturn_Negated_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithAssertInconclusive_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Inconclusive("This test only runs on Linux"); + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Linux)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckOnOSX_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.OSX)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenNotInTestMethod_NoDiagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + public void HelperMethod() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithOtherStatements_NoDiagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Some other logic + var x = 5; + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithElseBranch_NoDiagnostic() + { + string code = """ + using System; + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + else + { + // Do something + Console.WriteLine("Running on Windows"); + } + } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithSingleStatementReturn_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return;|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithLeadingComment_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + // Skip on Windows + [|if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithTrailingComment_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return; + }|] // This test only runs on Linux + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Linux)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithCommentInsideBlock_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Early exit for Windows + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckNotAtBeginningOfMethod_NoDiagnostic() + { + string code = """ + using System; + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + // arrange + Console.WriteLine("Setting up"); + + // some assertions + Assert.IsTrue(true); + + // if windows, return - this should NOT be flagged + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // some other assertions that are Windows only. + Console.WriteLine("Running Windows-only assertions"); + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenOperatingSystemIsWindows_Diagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!OperatingSystem.IsWindows()) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenOperatingSystemIsLinux_Diagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!OperatingSystem.IsLinux()) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Linux)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenOperatingSystemIsMacOS_Diagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!OperatingSystem.IsMacOS()) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.OSX)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenOperatingSystemIsWindows_NotNegated_Diagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (OperatingSystem.IsWindows()) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenOperatingSystemIsFreeBSD_Diagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + [|if (!OperatingSystem.IsFreeBSD()) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.FreeBSD)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenMethodAlreadyHasOSConditionAttribute_Diagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Linux)] + public void TestMethod() + { + [|if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + }|] + } + } + """; + + string fixedCode = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [OSCondition(OperatingSystems.Linux | OperatingSystems.Windows)] + public void TestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithNestedAssertions_NoDiagnostic() + { + string code = """ + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.IsTrue(true); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenOperatingSystemCheckWithNestedAssertions_NoDiagnostic() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + if (OperatingSystem.IsWindows()) + { + Assert.IsTrue(true); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } + + [TestMethod] + public async Task WhenRuntimeCheckWithNestedMultipleStatements_NoDiagnostic() + { + string code = """ + using System; + using System.Runtime.InteropServices; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Console.WriteLine("Windows-specific test"); + Assert.IsTrue(true); + } + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, code); + } +}