Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -11,3 +11,4 @@ MSTEST0021 | Design | Disabled | PreferDisposeOverTestCleanupAnalyzer, [Document
MSTEST0022 | Design | Disabled | PreferTestCleanupOverDisposeAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0022)
MSTEST0023 | Usage | Info | DoNotNegateBooleanAssertionAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0023)
MSTEST0024 | Usage | Info | DoNotStoreStaticTestContextAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0024)
MSTEST0025 | Usage | Info | PreferAssertFailOverAlwaysFalseConditionsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0025)
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 @@ -28,4 +28,5 @@ internal static class DiagnosticIds
public const string PreferTestCleanupOverDisposeRuleId = "MSTEST0022";
public const string DoNotNegateBooleanAssertionRuleId = "MSTEST0023";
public const string DoNotStoreStaticTestContextAnalyzerRuleId = "MSTEST0024";
public const string PreferAssertFailOverAlwaysFalseConditionsRuleId = "MSTEST0025";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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;

namespace MSTest.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class PreferAssertFailOverAlwaysFalseConditionsAnalyzer : DiagnosticAnalyzer
{
private enum EqualityStatus
{
Unknown,
Equal,
NotEqual,
}

private const string ExpectedParameterName = "expected";
private const string NotExpectedParameterName = "notExpected";
private const string ActualParameterName = "actual";
private const string ConditionParameterName = "condition";
private const string ValueParameterName = "value";

private static readonly LocalizableResourceString Title = new(nameof(Resources.PreferAssertFailOverAlwaysFalseConditionsTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.PreferAssertFailOverAlwaysFalseConditionsMessageFormat), Resources.ResourceManager, typeof(Resources));

internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
DiagnosticIds.PreferAssertFailOverAlwaysFalseConditionsRuleId,
Title,
MessageFormat,
null,
Category.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction(context =>
{
Compilation compilation = context.Compilation;
INamedTypeSymbol? assertSymbol = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssert);
if (assertSymbol is not null)
{
context.RegisterOperationAction(context => AnalyzeOperation(context, assertSymbol), OperationKind.Invocation);
}
});
}

private static void AnalyzeOperation(OperationAnalysisContext context, INamedTypeSymbol assertSymbol)
{
var operation = (IInvocationOperation)context.Operation;
if (assertSymbol.Equals(operation.TargetMethod?.ContainingType, SymbolEqualityComparer.Default) &&
IsAlwaysFalse(operation))
{
context.ReportDiagnostic(operation.CreateDiagnostic(Rule, operation.TargetMethod.Name));
}
}

private static bool IsAlwaysFalse(IInvocationOperation operation)
=> operation.TargetMethod.Name switch
{
"IsTrue" => GetConditionArgument(operation) is { Value.ConstantValue: { HasValue: true, Value: false } },
"IsFalse" => GetConditionArgument(operation) is { Value.ConstantValue: { HasValue: true, Value: true } },
"AreEqual" => GetEqualityStatus(operation, ExpectedParameterName) == EqualityStatus.NotEqual,
"AreNotEqual" => GetEqualityStatus(operation, NotExpectedParameterName) == EqualityStatus.Equal,
"IsNotNull" => GetValueArgument(operation) is { Value.ConstantValue: { HasValue: true, Value: null } },
_ => false,
};

private static IArgumentOperation? GetArgumentWithName(IInvocationOperation operation, string name)
=> operation.Arguments.FirstOrDefault(arg => arg.Parameter?.Name == name);

private static IArgumentOperation? GetConditionArgument(IInvocationOperation operation)
=> GetArgumentWithName(operation, ConditionParameterName);

private static IArgumentOperation? GetValueArgument(IInvocationOperation operation)
=> GetArgumentWithName(operation, ValueParameterName);

private static EqualityStatus GetEqualityStatus(IInvocationOperation operation, string expectedOrNotExpectedParameterName)
{
if (GetArgumentWithName(operation, expectedOrNotExpectedParameterName) is { } expectedOrNotExpectedArgument &&
GetArgumentWithName(operation, ActualParameterName) is { } actualArgument &&
expectedOrNotExpectedArgument.Value.ConstantValue.HasValue &&
actualArgument.Value.ConstantValue.HasValue)
{
return Equals(expectedOrNotExpectedArgument.Value.ConstantValue.Value, actualArgument.Value.ConstantValue.Value) ? EqualityStatus.Equal : EqualityStatus.NotEqual;
}

// We are not sure about the equality status
return EqualityStatus.Unknown;
}
}
18 changes: 18 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,12 @@
<data name="DoNotStoreStaticTestContextAnalyzerTitle" xml:space="preserve">
<value>Do not store TestContext in a static member</value>
</data>
<data name="PreferAssertFailOverAlwaysFalseConditionsMessageFormat" xml:space="preserve">
<value>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</value>
</data>
<data name="PreferAssertFailOverAlwaysFalseConditionsTitle" xml:space="preserve">
<value>Use 'Assert.Fail' instead of an always-failing assert</value>
</data>
<data name="PreferConstructorOverTestInitializeMessageFormat" xml:space="preserve">
<value>Prefer constructors over TestInitialize methods</value>
</data>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@
<target state="translated">Neukládejte TestContext ve statickém členu</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Upřednostňovat konstruktory před metodami TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">TestContext nicht in einem statischen Member speichern</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Konstruktoren gegenüber TestInitialize-Methoden bevorzugen</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">No almacenar TestContext en un miembro estático</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Preferir constructores en lugar de métodos TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">Ne pas stocker TestContext dans un membre statique</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Préférer les constructeurs aux méthodes TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">Non archiviare TestContext in un membro statico</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Preferisci costruttori rispetto ai metodi TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">静的メンバーに TestContext を格納しない</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">TestInitialize メソッドよりもコンストラクターを優先する</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">TestContext를 정적 멤버에 저장하지 마세요</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">TestInitialize 메서드보다 생성자 선호</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">Nie przechowuj elementu TestContext w statycznym elemencie członkowskim</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Preferowanie konstruktorów niż metod TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">Não armazene TestContext em um membro estático</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Preferir construtores em vez de métodos TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">Не хранить TestContext в статическом элементе</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Предпочитать конструкторы методам TestInitialize</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">TestContext'i statik üyede depolama</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">Oluşturucuları TestInitialize yöntemlerine tercih et</target>
Expand Down
10 changes: 10 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@
<target state="translated">不要将 TestContext 存储在静态成员中</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsMessageFormat">
<source>Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing 'Assert.{0}' assert</target>
<note />
</trans-unit>
<trans-unit id="PreferAssertFailOverAlwaysFalseConditionsTitle">
<source>Use 'Assert.Fail' instead of an always-failing assert</source>
<target state="new">Use 'Assert.Fail' instead of an always-failing assert</target>
<note />
</trans-unit>
<trans-unit id="PreferConstructorOverTestInitializeMessageFormat">
<source>Prefer constructors over TestInitialize methods</source>
<target state="translated">首选构造函数而不是 TestInitialize 方法</target>
Expand Down
Loading