diff --git a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
index f2b7fad657..db7d4e4da2 100644
--- a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md
@@ -1,2 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+MSTEST0058 | Usage | Info | AvoidAssertsInCatchBlocksAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0058)
diff --git a/src/Analyzers/MSTest.Analyzers/AvoidAssertsInCatchBlocksAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/AvoidAssertsInCatchBlocksAnalyzer.cs
new file mode 100644
index 0000000000..d7316bd9a8
--- /dev/null
+++ b/src/Analyzers/MSTest.Analyzers/AvoidAssertsInCatchBlocksAnalyzer.cs
@@ -0,0 +1,99 @@
+// 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;
+
+///
+/// MSTEST0058: .
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+public sealed class AvoidAssertsInCatchBlocksAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly LocalizableResourceString Title = new(nameof(Resources.AvoidAssertsInCatchBlocksTitle), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.AvoidAssertsInCatchBlocksMessageFormat), Resources.ResourceManager, typeof(Resources));
+ private static readonly LocalizableResourceString Description = new(nameof(Resources.AvoidAssertsInCatchBlocksDescription), Resources.ResourceManager, typeof(Resources));
+
+ internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
+ DiagnosticIds.AvoidAssertsInCatchBlocksRuleId,
+ Title,
+ MessageFormat,
+ Description,
+ Category.Usage,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; }
+ = ImmutableArray.Create(Rule);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+ context.RegisterCompilationStartAction(context =>
+ {
+ Compilation compilation = context.Compilation;
+ INamedTypeSymbol? assertSymbol = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssert);
+ INamedTypeSymbol? stringAssertSymbol = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingStringAssert);
+ INamedTypeSymbol? collectionAssertSymbol = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingCollectionAssert);
+
+ if (assertSymbol is not null || stringAssertSymbol is not null || collectionAssertSymbol is not null)
+ {
+ context.RegisterOperationAction(context => AnalyzeOperation(context, assertSymbol, stringAssertSymbol, collectionAssertSymbol), OperationKind.Invocation);
+ }
+ });
+ }
+
+ private static void AnalyzeOperation(
+ OperationAnalysisContext context,
+ INamedTypeSymbol? assertSymbol,
+ INamedTypeSymbol? stringAssertSymbol,
+ INamedTypeSymbol? collectionAssertSymbol)
+ {
+ var operation = (IInvocationOperation)context.Operation;
+ INamedTypeSymbol targetType = operation.TargetMethod.ContainingType;
+ bool isAssertType =
+ targetType.Equals(assertSymbol, SymbolEqualityComparer.Default) ||
+ targetType.Equals(stringAssertSymbol, SymbolEqualityComparer.Default) ||
+ targetType.Equals(collectionAssertSymbol, SymbolEqualityComparer.Default);
+
+ if (!isAssertType)
+ {
+ return;
+ }
+
+ // Walk up the operation tree to check if we're inside a catch clause
+ if (IsInsideCatchClause(operation))
+ {
+ context.ReportDiagnostic(operation.CreateDiagnostic(Rule));
+ }
+ }
+
+ private static bool IsInsideCatchClause(IOperation operation)
+ {
+ IOperation? current = operation;
+ while (current is not null)
+ {
+ if (current is ICatchClauseOperation)
+ {
+ return true;
+ }
+
+ current = current.Parent;
+ }
+
+ return false;
+ }
+}
diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
index d7f58d9e67..15b809366e 100644
--- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
+++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
@@ -62,4 +62,5 @@ internal static class DiagnosticIds
public const string IgnoreStringMethodReturnValueRuleId = "MSTEST0055";
public const string TestMethodAttributeShouldSetDisplayNameCorrectlyRuleId = "MSTEST0056";
public const string TestMethodAttributeShouldPropagateSourceInformationRuleId = "MSTEST0057";
+ public const string AvoidAssertsInCatchBlocksRuleId = "MSTEST0058";
}
diff --git a/src/Analyzers/MSTest.Analyzers/Resources.resx b/src/Analyzers/MSTest.Analyzers/Resources.resx
index 0c8a17d969..85df4da66e 100644
--- a/src/Analyzers/MSTest.Analyzers/Resources.resx
+++ b/src/Analyzers/MSTest.Analyzers/Resources.resx
@@ -684,4 +684,13 @@ The type declaring these methods should also respect the following rules:
Methods like Contains, StartsWith, and EndsWith return boolean values that indicate whether the condition was met. Ignoring these return values is likely a mistake.
+
+ Do not use asserts in catch blocks
+
+
+ Do not use asserts in catch blocks because they may not fail the test if no exception is thrown
+
+
+ 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.
+
\ No newline at end of file
diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
index ba62836f66..b7b156acca 100644
--- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
+++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
@@ -989,6 +989,21 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla:
Parametr Assert.Throws by měl obsahovat jenom jeden příkaz nebo výraz
+
+ Do not use asserts in catch blocks
+ Do not use asserts in catch blocks
+
+
+
+ Do not use asserts in catch blocks because they may not fail the test if no exception is thrown
+ Do not use asserts in catch blocks because they may not fail the test if no exception is thrown
+
+
+
+ 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.
+ 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.
+
+