Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCS1140 - Don't require user to document exception types if caught in same method #1524

Merged
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fix analyzer [RCS1202](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1202) ([PR](https://github.com/dotnet/roslynator/pull/1542))
- 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -216,4 +219,32 @@ private static bool InheritsFromException(ITypeSymbol typeSymbol, INamedTypeSymb
&& typeSymbol.BaseType?.IsObject() == false
&& typeSymbol.InheritsFrom(exceptionSymbol);
}

/// <summary>
/// Walk upwards from throw statement and find all try statements in method and see if any of them catches the thrown exception type
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
@@ -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<AddExceptionToDocumentationCommentAnalyzer, AddExceptionToDocumentationCommentCodeFixProvider>
{
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
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
public void Foo(object parameter)
{
if (parameter == null)
[|throw new ArgumentNullException(nameof(parameter));|]
}
}

""", """
using System;

class C
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
/// <exception cref="ArgumentNullException"><paramref name="parameter"/> is <c>null</c>.</exception>
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
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
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
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
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
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
public void Foo(object parameter)
{
try
{
if (parameter == null)
[|throw new ArgumentNullException(nameof(parameter));|]
}
catch (InvalidOperationException) {}
}
}

""", """
using System;

class C
{
/// <summary>
/// ...
/// </summary>
/// <param name="parameter"></param>
/// <exception cref="ArgumentNullException"><paramref name="parameter"/> is <c>null</c>.</exception>
public void Foo(object parameter)
{
try
{
if (parameter == null)
throw new ArgumentNullException(nameof(parameter));
}
catch (InvalidOperationException) {}
}
}

""");
}
}