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 1b10a2ebcb..7039e55524 100644 --- a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md @@ -6,4 +6,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- MSTEST0058 | Usage | Info | AvoidAssertsInCatchBlocksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0058) -MSTEST0059 | Usage | Warning | DoNotUseParallelizeAndDoNotParallelizeTogetherAnalyzer +MSTEST0059 | Usage | Warning | DoNotUseParallelizeAndDoNotParallelizeTogetherAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0059) +MSTEST0061 | Usage | Info | UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0061) diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs index fecaa26f79..6954f5b376 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs @@ -64,4 +64,5 @@ internal static class DiagnosticIds public const string TestMethodAttributeShouldPropagateSourceInformationRuleId = "MSTEST0057"; public const string AvoidAssertsInCatchBlocksRuleId = "MSTEST0058"; public const string DoNotUseParallelizeAndDoNotParallelizeTogetherRuleId = "MSTEST0059"; + public const string UseOSConditionAttributeInsteadOfRuntimeCheckRuleId = "MSTEST0061"; } 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 df777849d1..d99204ee76 100644 --- a/src/Analyzers/MSTest.Analyzers/Resources.resx +++ b/src/Analyzers/MSTest.Analyzers/Resources.resx @@ -711,4 +711,13 @@ The type declaring these methods should also respect the following rules: An assembly should have either '[Parallelize]' or '[DoNotParallelize]' attribute, but not both. Having both attributes creates an ambiguous configuration. When both are present, '[DoNotParallelize]' takes precedence and parallelization will be disabled. {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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..3d9fae07cc --- /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; + +/// +/// MSTEST0061: 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 ed51a42787..f2c7a63515 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf @@ -1029,6 +1029,21 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf index 1a88c91be1..53610df653 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf @@ -1030,6 +1030,21 @@ Der Typ, der diese Methoden deklariert, sollte auch die folgenden Regeln beachte Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf index a6234d2ef9..a81b85e9c7 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf @@ -1029,6 +1029,21 @@ El tipo que declara estos métodos también debe respetar las reglas siguientes: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf index 0211724146..a3330e8791 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf @@ -1029,6 +1029,21 @@ Le type doit être une classe Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf index c42f00d3d5..d1b7126688 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf @@ -1029,6 +1029,21 @@ Anche il tipo che dichiara questi metodi deve rispettare le regole seguenti: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf index f77338dade..f50cd2ec24 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf @@ -1029,6 +1029,21 @@ The type declaring these methods should also respect the following rules: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf index 9547a4fbd5..40a0ee3355 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf @@ -1029,6 +1029,21 @@ The type declaring these methods should also respect the following rules: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf index 9a556ade40..028f3984c1 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf @@ -1029,6 +1029,21 @@ Typ deklarujący te metody powinien również przestrzegać następujących regu Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf index 26fa3e8a15..3dcbecb125 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf @@ -1029,6 +1029,21 @@ O tipo que declara esses métodos também deve respeitar as seguintes regras: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf index dfae50532e..baf9d18984 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf @@ -1041,6 +1041,21 @@ The type declaring these methods should also respect the following rules: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf index 6e7928241e..861b2c652b 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf @@ -1031,6 +1031,21 @@ Bu yöntemleri bildiren tipin ayrıca aşağıdaki kurallara uyması gerekir: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf index 30ca3c3b4f..4c16bad3f8 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf @@ -1029,6 +1029,21 @@ The type declaring these methods should also respect the following rules: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf index 39684d91c9..5ffd10ab8e 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf @@ -1029,6 +1029,21 @@ The type declaring these methods should also respect the following rules: Do not use both '[Parallelize]' and '[DoNotParallelize]' attributes {Locked="[Parallelize]"}{Locked="[DoNotParallelize]"} + + 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. + + \ No newline at end of file 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); + } +}