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

Add analyzer and fix provider RCS1265: Remove redundant catch statement #1364

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

- Publish NuGet packages that provide [refactorings](https://www.nuget.org/packages/roslynator.refactorings) and [code fixes for compiler diagnostics](https://www.nuget.org/packages/roslynator.codefixes) ([PR](https://github.com/dotnet/roslynator/pull/1358))
- These packages are recommended to be used in an environment where Roslynator IDE extension cannot be used, e.g. VS Code + C# Dev Kit (see related [issue](https://github.com/dotnet/vscode-csharp/issues/6790))
- Add analyzer "Remove redundant catch block" [RCS1265](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1265) ([PR](https://github.com/dotnet/roslynator/pull/1364) by @jakubreznak)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CodeFixes;

namespace Roslynator.CSharp.CSharp.CodeFixes;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveRedundantCatchBlockCodeFixProvider))]
[Shared]
public class RemoveRedundantCatchBlockCodeFixProvider : BaseCodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(DiagnosticRules.RemoveRedundantCatchBlock.Id);

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
SyntaxNode root = await context.Document.GetSyntaxRootAsync().ConfigureAwait(false);

if (!TryFindFirstAncestorOrSelf(root, context.Span, out CatchClauseSyntax catchClause))
return;

context.RegisterCodeFix(
CodeAction.Create(
title: "Remove redundant catch block",
createChangedDocument: c => RemoveRedundantCatchAsync(context.Document, catchClause, c),
equivalenceKey: nameof(RemoveRedundantCatchBlockCodeFixProvider)),
context.Diagnostics[0]);
}

private static async Task<Document> RemoveRedundantCatchAsync(Document document, CatchClauseSyntax catchClause, CancellationToken cancellationToken)
{
var tryStatement = (TryStatementSyntax)catchClause.Parent;
SyntaxList<CatchClauseSyntax> catchClauses = tryStatement.Catches;

if (catchClauses.Count == 1 && tryStatement.Finally is null)
{
IEnumerable<StatementSyntax> newNodes = tryStatement
.Block
.Statements
.Select(f => f.WithFormatterAnnotation());

newNodes = new[] { newNodes.First().WithLeadingTrivia(tryStatement.GetLeadingTrivia()) }.Concat(newNodes.Skip(1));

return await document.ReplaceNodeAsync(tryStatement, newNodes, cancellationToken).ConfigureAwait(false);
}
else
{
TryStatementSyntax newTryStatement = tryStatement.RemoveNode(catchClauses.Last(), SyntaxRemoveOptions.KeepNoTrivia);
return await document.ReplaceNodeAsync(tryStatement, newTryStatement, cancellationToken).ConfigureAwait(false);
}
}
}
35 changes: 35 additions & 0 deletions src/Analyzers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7656,6 +7656,41 @@ public string Foo(string bar)
<Option Key="use_var" IsRequired="true" />
</ConfigOptions>
</Analyzer>
<Analyzer>
<Id>RCS1265</Id>
<Identifier>RemoveRedundantCatchBlock</Identifier>
<Title>Remove redundant catch block</Title>
<DefaultSeverity>Info</DefaultSeverity>
<IsEnabledByDefault>true</IsEnabledByDefault>
<Samples>
<Sample>
<Before><![CDATA[
try
{
DoSomething();
}
catch
{
throw;
}
finally
{
DoSomethingElse();
}
]]></Before>
<After><![CDATA[
try
{
DoSomething();
}
finally
{
DoSomethingElse();
}
]]></After>
</Sample>
</Samples>
</Analyzer>
<Analyzer>
<Id>RCS9001</Id>
<Identifier>UsePatternMatching</Identifier>
Expand Down
78 changes: 78 additions & 0 deletions src/Analyzers/CSharp/Analysis/RemoveRedundantCatchBlockAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslynator.CSharp.Analysis;

namespace Roslynator.CSharp.CSharp.Analysis;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RemoveRedundantCatchBlockAnalyzer : BaseDiagnosticAnalyzer
{
private static ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
if (_supportedDiagnostics.IsDefault)
{
Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.RemoveRedundantCatchBlock);
}

return _supportedDiagnostics;
}
}

public override void Initialize(AnalysisContext context)
{
base.Initialize(context);

context.RegisterSyntaxNodeAction(f => AnalyzeCatchClause(f), SyntaxKind.TryStatement);
}

private static void AnalyzeCatchClause(SyntaxNodeAnalysisContext context)
josefpihrt marked this conversation as resolved.
Show resolved Hide resolved
{
var tryStatement = (TryStatementSyntax)context.Node;

if (!tryStatement.Catches.Any())
return;

CatchClauseSyntax lastCatchClause = tryStatement.Catches.Last();

if (lastCatchClause.Declaration is not null)
return;

if (lastCatchClause.Block?.Statements.Count != 1)
return;

if (lastCatchClause.Block.Statements[0] is not ThrowStatementSyntax throwStatement || throwStatement.Expression is not null)
return;

if (tryStatement.Catches.Count > 1 || tryStatement.Finally is not null)
{
DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveRedundantCatchBlock, lastCatchClause);
}
else
{
BlockSyntax tryBlock = tryStatement.Block;

if (tryBlock?.Statements.Any() != true)
return;

if (!SyntaxTriviaAnalysis.IsExteriorTriviaEmptyOrWhitespace(tryBlock.OpenBraceToken))
return;

if (!SyntaxTriviaAnalysis.IsExteriorTriviaEmptyOrWhitespace(tryBlock.CloseBraceToken))
return;

if (!lastCatchClause.CatchKeyword.LeadingTrivia.IsEmptyOrWhitespace())
return;

DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveRedundantCatchBlock, lastCatchClause);
}
}
}
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,6 @@ public static partial class DiagnosticIdentifiers
public const string UnnecessaryRawStringLiteral = "RCS1262";
public const string InvalidReferenceInDocumentationComment = "RCS1263";
public const string UseVarOrExplicitType = "RCS1264";
public const string RemoveRedundantCatchBlock = "RCS1265";
}
}
12 changes: 12 additions & 0 deletions src/Analyzers/CSharp/DiagnosticRules.Generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2615,5 +2615,17 @@ public static partial class DiagnosticRules
helpLinkUri: DiagnosticIdentifiers.UseVarOrExplicitType,
customTags: Array.Empty<string>());

/// <summary>RCS1265</summary>
public static readonly DiagnosticDescriptor RemoveRedundantCatchBlock = DiagnosticDescriptorFactory.Create(
id: DiagnosticIdentifiers.RemoveRedundantCatchBlock,
title: "Remove redundant catch block",
messageFormat: "Remove redundant catch block",
category: DiagnosticCategories.Roslynator,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: null,
helpLinkUri: DiagnosticIdentifiers.RemoveRedundantCatchBlock,
customTags: Array.Empty<string>());

}
}
179 changes: 179 additions & 0 deletions src/Tests/Analyzers.Tests/RCS1265RemoveRedundantCatchBlockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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.CSharp.Analysis;
using Roslynator.CSharp.CSharp.CodeFixes;
using Roslynator.Testing.CSharp;
using Xunit;

namespace Roslynator.CSharp.Analysis.Tests;

public class RCS1265RemoveRedundantCatchBlockTests : AbstractCSharpDiagnosticVerifier<RemoveRedundantCatchBlockAnalyzer, RemoveRedundantCatchBlockCodeFixProvider>
{
public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.RemoveRedundantCatchBlock;

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantCatchBlock)]
public async Task Test_TryCatchFinally()
{
await VerifyDiagnosticAndFixAsync(@"
class C
{
void M()
{
try
{
DoSomething();
}
[|catch
{
throw;
}|]
finally
{
DoSomething();
}
}

void DoSomething()
{
}
}
", @"
class C
{
void M()
{
try
{
DoSomething();
}
finally
{
DoSomething();
}
}

void DoSomething()
{
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantCatchBlock)]
public async Task Test_TryMultipleCatches()
{
await VerifyDiagnosticAndFixAsync(@"
using System;

class C
{
void M()
{
try
{
DoSomething();
}
catch (SystemException ex)
{
DoSomething();
}
[|catch
{
throw;
}|]
}

void DoSomething()
{
}
}
", @"
using System;

class C
{
void M()
{
try
{
DoSomething();
}
catch (SystemException ex)
{
DoSomething();
}
}

void DoSomething()
{
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantCatchBlock)]
public async Task Test_TryCatch()
{
await VerifyDiagnosticAndFixAsync(@"
class C
{
void M()
{
try
{
DoSomething();
}
[|catch
{
throw;
}|]
}

void DoSomething()
{
}
}
", @"
class C
{
void M()
{
DoSomething();
}

void DoSomething()
{
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveRedundantCatchBlock)]
public async Task Test_NoDiagnostic()
{
await VerifyNoDiagnosticAsync(@"
using System;

class C
{
void M()
{
try
{
DoSomething();
}
catch
{
throw new SystemException();
}

void DoSomething()
{
}
}
}
");
}
}