diff --git a/ChangeLog.md b/ChangeLog.md
index 60cf048e59..53c8de6666 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix analyzer [RCS1202](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1202) ([PR](https://github.com/dotnet/roslynator/pull/1542))
- Fix analyzer [RCS1246](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1246) ([PR](https://github.com/dotnet/roslynator/pull/1543))
+- Fix analyzer [RCS1140](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1140) ([PR](https://github.com/dotnet/roslynator/pull/1524))
## [4.12.6] - 2024-09-23
diff --git a/src/Common/CSharp/Analysis/AddExceptionToDocumentationComment/AddExceptionToDocumentationCommentAnalysis.cs b/src/Common/CSharp/Analysis/AddExceptionToDocumentationComment/AddExceptionToDocumentationCommentAnalysis.cs
index a1ab936454..448dd2159f 100644
--- a/src/Common/CSharp/Analysis/AddExceptionToDocumentationComment/AddExceptionToDocumentationCommentAnalysis.cs
+++ b/src/Common/CSharp/Analysis/AddExceptionToDocumentationComment/AddExceptionToDocumentationCommentAnalysis.cs
@@ -61,6 +61,9 @@ private static AddExceptionToDocumentationCommentAnalysisResult Analyze(
if (!InheritsFromException(typeSymbol, exceptionSymbol))
return Fail;
+ if (IsExceptionTypeCaughtInMethod(node, typeSymbol, semanticModel, cancellationToken))
+ return Fail;
+
ISymbol declarationSymbol = GetDeclarationSymbol(node.SpanStart, semanticModel, cancellationToken);
if (declarationSymbol?.GetSyntax(cancellationToken) is not MemberDeclarationSyntax containingMember)
@@ -216,4 +219,32 @@ private static bool InheritsFromException(ITypeSymbol typeSymbol, INamedTypeSymb
&& typeSymbol.BaseType?.IsObject() == false
&& typeSymbol.InheritsFrom(exceptionSymbol);
}
+
+ ///
+ /// Walk upwards from throw statement and find all try statements in method and see if any of them catches the thrown exception type
+ ///
+ private static bool IsExceptionTypeCaughtInMethod(SyntaxNode node, ITypeSymbol exceptionSymbol, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ SyntaxNode parent = node.Parent;
+ while (parent is not null)
+ {
+ if (parent is TryStatementSyntax tryStatement && tryStatement.Catches.Any(catchClause => SymbolEqualityComparer.Default.Equals(exceptionSymbol, semanticModel.GetTypeSymbol(catchClause.Declaration?.Type, cancellationToken))))
+ {
+ return true;
+ }
+
+ if (parent is MemberDeclarationSyntax or LocalFunctionStatementSyntax)
+ {
+ // We don't care if it's caught outside of the current method
+ // Since the exception should be documented in this method
+ return false;
+ }
+ else
+ {
+ parent = parent.Parent;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/Tests/Analyzers.Tests/RCS1140AddExceptionToDocumentationCommentTests.cs b/src/Tests/Analyzers.Tests/RCS1140AddExceptionToDocumentationCommentTests.cs
new file mode 100644
index 0000000000..889f45b193
--- /dev/null
+++ b/src/Tests/Analyzers.Tests/RCS1140AddExceptionToDocumentationCommentTests.cs
@@ -0,0 +1,154 @@
+// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Roslynator.CSharp.CodeFixes;
+using Roslynator.Testing.CSharp;
+using Xunit;
+
+namespace Roslynator.CSharp.Analysis.Tests;
+
+public class RCS1140AddExceptionToDocumentationCommentTests : AbstractCSharpDiagnosticVerifier
+{
+ public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.AddExceptionToDocumentationComment;
+
+ [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddExceptionToDocumentationComment)]
+ public async Task Test_Example_From_Documentation()
+ {
+ await VerifyDiagnosticAndFixAsync("""
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ public void Foo(object parameter)
+ {
+ if (parameter == null)
+ [|throw new ArgumentNullException(nameof(parameter));|]
+ }
+}
+
+""", """
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ /// is null.
+ public void Foo(object parameter)
+ {
+ if (parameter == null)
+ throw new ArgumentNullException(nameof(parameter));
+ }
+}
+
+""");
+ }
+
+ [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddExceptionToDocumentationComment)]
+ public async Task Test_No_Diagnostic_If_Exception_Is_Caught_In_Method()
+ {
+ await VerifyNoDiagnosticAsync("""
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ public void Foo(object parameter)
+ {
+ try
+ {
+ if (parameter == null)
+ throw new ArgumentNullException(nameof(parameter));
+ }
+ catch (ArgumentNullException) {}
+ }
+}
+""");
+ }
+
+ [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddExceptionToDocumentationComment)]
+ public async Task Test_No_Diagnostic_If_Exception_Is_Caught_In_Method_Nested()
+ {
+ await VerifyNoDiagnosticAsync("""
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ public void Foo(object parameter)
+ {
+ try
+ {
+ try
+ {
+ if (parameter == null)
+ throw new ArgumentNullException(nameof(parameter));
+ }
+ catch (InvalidOperationException) {}
+ }
+ catch (ArgumentNullException) {}
+ }
+}
+""");
+ }
+
+ [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.AddExceptionToDocumentationComment)]
+ public async Task Test_Diagnostic_If_Not_Correct_Exception_Is_Caught_In_Method()
+ {
+ await VerifyDiagnosticAndFixAsync("""
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ public void Foo(object parameter)
+ {
+ try
+ {
+ if (parameter == null)
+ [|throw new ArgumentNullException(nameof(parameter));|]
+ }
+ catch (InvalidOperationException) {}
+ }
+}
+
+""", """
+using System;
+
+class C
+{
+ ///
+ /// ...
+ ///
+ ///
+ /// is null.
+ public void Foo(object parameter)
+ {
+ try
+ {
+ if (parameter == null)
+ throw new ArgumentNullException(nameof(parameter));
+ }
+ catch (InvalidOperationException) {}
+ }
+}
+
+""");
+ }
+}