Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,7 @@
<data name="UseCancellationTokenPropertyFix" xml:space="preserve">
<value>Use 'TestContext.CancellationToken' instead</value>
</data>
<data name="UseOSConditionAttributeInsteadOfRuntimeCheckFix" xml:space="preserve">
<value>Use '[OSCondition]' attribute</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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;

/// <summary>
/// Code fixer for <see cref="UseOSConditionAttributeInsteadOfRuntimeCheckAnalyzer"/>.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseOSConditionAttributeInsteadOfRuntimeCheckFixer))]
[Shared]
public sealed class UseOSConditionAttributeInsteadOfRuntimeCheckFixer : CodeFixProvider
{
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds { get; }
= ImmutableArray.Create(DiagnosticIds.UseOSConditionAttributeInsteadOfRuntimeCheckRuleId);

/// <inheritdoc />
public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
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<MethodDeclarationSyntax>();
if (methodDeclaration is null)
{
return;
}

// Find the if statement to remove
IfStatementSyntax? ifStatement = diagnosticNode.FirstAncestorOrSelf<IfStatementSyntax>();
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<Document> AddOSConditionAttributeAsync(
Document document,
MethodDeclarationSyntax methodDeclaration,
IfStatementSyntax ifStatement,
string osPlatform,
bool isNegated,
CancellationToken cancellationToken)
{
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// Map OSPlatform to OperatingSystems enum
string? operatingSystem = MapOSPlatformToOperatingSystem(osPlatform);
if (operatingSystem is null)
{
return document;
}

// Determine the condition mode:
// - If isNegated is false (checking if IS on platform, then early return), we want to EXCLUDE this platform
// - If isNegated is true (checking if NOT on platform, then early return), we want to INCLUDE this platform only
// Actually:
// if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; => Include Windows only (default, omit ConditionMode)
// if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; => Exclude Windows

// Create the attribute
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}")),
})));
}

// Check if the method already has an OSCondition attribute
bool hasOSConditionAttribute = methodDeclaration.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString() is "OSCondition" or "OSConditionAttribute");

// Track the if statement on the original method before making any modifications
MethodDeclarationSyntax trackedMethod = methodDeclaration.TrackNodes(ifStatement);
IfStatementSyntax? trackedIfStatement = trackedMethod.GetCurrentNode(ifStatement);

if (trackedIfStatement is null)
{
return document;
}

// Remove the if statement from the method body
MethodDeclarationSyntax newMethod = trackedMethod.RemoveNode(trackedIfStatement, SyntaxRemoveOptions.KeepNoTrivia)!;

if (!hasOSConditionAttribute)
{
// Add the attribute to the method
// Use CarriageReturnLineFeed to match the formatting of attributes added by the editor
AttributeListSyntax newAttributeList = SyntaxFactory.AttributeList(
SyntaxFactory.SingletonSeparatedList(osConditionAttribute))
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);

newMethod = newMethod.AddAttributeLists(newAttributeList);
}

// Replace the entire method declaration with the modified version
editor.ReplaceNode(methodDeclaration, newMethod);

return editor.GetChangedDocument();
}

private static string? MapOSPlatformToOperatingSystem(string osPlatform)
=> osPlatform.ToUpperInvariant() switch
{
"WINDOWS" => "Windows",
"LINUX" => "Linux",
"OSX" => "OSX",
"FREEBSD" => "FreeBSD",
_ => null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Použít {0}</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">"{0}" verwenden</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Usar "{0}"</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Utiliser « {0} »</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Usa '{0}'</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">'{0}' を使用します</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">'{0}' 사용</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Użyj „{0}”</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Usar '{0}'</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">Использовать "{0}"</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">'{0}' kullan</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">使用“{0}”</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<source>Use '{0}'</source>
<target state="translated">使用 '{0}'</target>
<note />
</trans-unit>
<trans-unit id="UseOSConditionAttributeInsteadOfRuntimeCheckFix">
<source>Use '[OSCondition]' attribute</source>
<target state="new">Use '[OSCondition]' attribute</target>
<note />
</trans-unit>
</body>
</file>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
3 changes: 3 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
9 changes: 9 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -693,4 +693,13 @@ The type declaring these methods should also respect the following rules:
<data name="AvoidAssertsInCatchBlocksDescription" xml:space="preserve">
<value>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.</value>
</data>
<data name="UseOSConditionAttributeInsteadOfRuntimeCheckTitle" xml:space="preserve">
<value>Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive'</value>
</data>
<data name="UseOSConditionAttributeInsteadOfRuntimeCheckMessageFormat" xml:space="preserve">
<value>Use '[OSCondition]' attribute instead of 'RuntimeInformation.IsOSPlatform' calls with early return or 'Assert.Inconclusive'</value>
</data>
<data name="UseOSConditionAttributeInsteadOfRuntimeCheckDescription" xml:space="preserve">
<value>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.</value>
</data>
</root>
Loading
Loading