From bde648773354879f83efc2c822af5440174cb851 Mon Sep 17 00:00:00 2001 From: Qluxzz Date: Fri, 27 Sep 2024 16:05:48 +0200 Subject: [PATCH] RCS1140 - Don't require user to document exception types if caught in same method (#1524) --- ChangeLog.md | 1 + ...ExceptionToDocumentationCommentAnalysis.cs | 31 ++++ ...AddExceptionToDocumentationCommentTests.cs | 154 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 src/Tests/Analyzers.Tests/RCS1140AddExceptionToDocumentationCommentTests.cs 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) {} + } +} + +"""); + } +}