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

Fix S2737 FP: Raised when exception filter is used #9385

Merged
merged 4 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions analyzers/src/SonarAnalyzer.CSharp/Rules/CatchRethrow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,27 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Rules.CSharp
namespace SonarAnalyzer.Rules.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CatchRethrow : CatchRethrowBase<SyntaxKind, CatchClauseSyntax>
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CatchRethrow : CatchRethrowBase<CatchClauseSyntax>
{
private static readonly BlockSyntax ThrowBlock = SyntaxFactory.Block(SyntaxFactory.ThrowStatement());
private static readonly BlockSyntax ThrowBlock = SyntaxFactory.Block(SyntaxFactory.ThrowStatement());

protected override DiagnosticDescriptor Rule { get; } =
DescriptorFactory.Create(DiagnosticId, MessageFormat);
protected override ILanguageFacade<SyntaxKind> Language => CSharpFacade.Instance;

protected override bool ContainsOnlyThrow(CatchClauseSyntax currentCatch) =>
CSharpEquivalenceChecker.AreEquivalent(currentCatch.Block, ThrowBlock);
protected override bool ContainsOnlyThrow(CatchClauseSyntax currentCatch) =>
CSharpEquivalenceChecker.AreEquivalent(currentCatch.Block, ThrowBlock);

protected override IReadOnlyList<CatchClauseSyntax> GetCatches(SyntaxNode syntaxNode) =>
((TryStatementSyntax)syntaxNode).Catches;
protected override CatchClauseSyntax[] AllCatches(SyntaxNode node) =>
((TryStatementSyntax)node).Catches.ToArray();

protected override SyntaxNode GetDeclarationType(CatchClauseSyntax catchClause) =>
catchClause.Declaration?.Type;
protected override SyntaxNode DeclarationType(CatchClauseSyntax catchClause) =>
catchClause.Declaration?.Type;

protected override bool HasFilter(CatchClauseSyntax catchClause) =>
catchClause.Filter != null;
protected override bool HasFilter(CatchClauseSyntax catchClause) =>
catchClause.Filter is not null;

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(RaiseOnInvalidCatch, SyntaxKind.TryStatement);
}
protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(RaiseOnInvalidCatch, SyntaxKind.TryStatement);
}
119 changes: 44 additions & 75 deletions analyzers/src/SonarAnalyzer.Common/Rules/CatchRethrowBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,100 +18,69 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Rules
{
public abstract class CatchRethrowBase<TCatchClause> : SonarDiagnosticAnalyzer
where TCatchClause : SyntaxNode
{
internal const string DiagnosticId = "S2737";
protected const string MessageFormat = "Add logic to this catch clause or eliminate it and rethrow the exception automatically.";
namespace SonarAnalyzer.Rules;

protected abstract DiagnosticDescriptor Rule { get; }
public abstract class CatchRethrowBase<TSyntaxKind, TCatchClause> : SonarDiagnosticAnalyzer<TSyntaxKind>
where TSyntaxKind : struct
where TCatchClause : SyntaxNode
{
internal const string DiagnosticId = "S2737";

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
protected abstract TCatchClause[] AllCatches(SyntaxNode node);
protected abstract SyntaxNode DeclarationType(TCatchClause catchClause);
protected abstract bool HasFilter(TCatchClause catchClause);
protected abstract bool ContainsOnlyThrow(TCatchClause currentCatch);

protected abstract IReadOnlyList<TCatchClause> GetCatches(SyntaxNode syntaxNode);
protected override string MessageFormat => "Add logic to this catch clause or eliminate it and rethrow the exception automatically.";

protected abstract bool HasFilter(TCatchClause catchClause);
protected CatchRethrowBase() : base(DiagnosticId) { }

protected abstract SyntaxNode GetDeclarationType(TCatchClause catchClause);
protected void RaiseOnInvalidCatch(SonarSyntaxNodeReportingContext context)
{
var catches = AllCatches(context.Node);
var caughtExceptionTypes = new Lazy<INamedTypeSymbol[]>(() => ComputeExceptionTypes(catches, context.SemanticModel));
var redundantCatches = new HashSet<TCatchClause>();
// We handle differently redundant catch clauses (just throw inside) that are followed by a non-redundant catch clause, because if they are removed, the method behavior will change.
var followingCatchesOnlyThrow = true;

protected void RaiseOnInvalidCatch(SonarSyntaxNodeReportingContext context)
for (var i = catches.Length - 1; i >= 0; i--)
{
var catches = GetCatches(context.Node);
var caughtExceptionTypes = new Lazy<List<INamedTypeSymbol>>(() =>
ComputeExceptionTypesIfNeeded(catches, context.SemanticModel));
var redundantCatches = new HashSet<TCatchClause>();

// We handle differently redundant catch clauses (just throw inside) that are
// followed by a non-redundant catch clause, because if they are removed, the
// method behavior will change.
var foundNotRedundantCatch = false;

for (var i = catches.Count - 1; i >= 0; i--)
var currentCatch = catches[i];
if (ContainsOnlyThrow(currentCatch))
{
var currentCatch = catches[i];
if (ContainsOnlyThrow(currentCatch))
{
if (foundNotRedundantCatch)
{
// Make sure we report only catch clauses that will not change
// the method behavior if removed.
if (!HasFilter(currentCatch) &&
!IsMoreSpecificTypeThanANotRedundantCatch(i, catches, caughtExceptionTypes.Value, redundantCatches))
{
redundantCatches.Add(currentCatch);
}
}
else
{
redundantCatches.Add(currentCatch);
}
}
else
if (!HasFilter(currentCatch)
// Make sure we report only catch clauses that will not change the method behavior if removed.
&& (followingCatchesOnlyThrow || IsRedundantToFollowingCatches(i, catches, caughtExceptionTypes.Value, redundantCatches)))
{
foundNotRedundantCatch = true;
redundantCatches.Add(currentCatch);
}
}

foreach (var redundantCatch in redundantCatches)
else
{
context.ReportIssue(Diagnostic.Create(Rule, redundantCatch.GetLocation()));
followingCatchesOnlyThrow = false;
}
}

protected abstract bool ContainsOnlyThrow(TCatchClause currentCatch);

private static bool IsMoreSpecificTypeThanANotRedundantCatch(int catchIndex, IReadOnlyList<TCatchClause> catches,
List<INamedTypeSymbol> caughtExceptionTypes, ISet<TCatchClause> redundantCatches)
foreach (var redundantCatch in redundantCatches)
{
var currentType = caughtExceptionTypes[catchIndex];
for (var i = catchIndex + 1; i < caughtExceptionTypes.Count; i++)
{
var nextCatchType = caughtExceptionTypes[i];

if (nextCatchType == null ||
currentType.DerivesOrImplements(nextCatchType))
{
return !redundantCatches.Contains(catches[i]);
}
}
return false;
context.ReportIssue(Rule, redundantCatch);
}
}

private List<INamedTypeSymbol> ComputeExceptionTypesIfNeeded(IEnumerable<TCatchClause> catches,
SemanticModel semanticModel)
private static bool IsRedundantToFollowingCatches(int catchIndex, TCatchClause[] catches, INamedTypeSymbol[] caughtExceptionTypes, ISet<TCatchClause> redundantCatches)
{
var currentType = caughtExceptionTypes[catchIndex];
for (var i = catchIndex + 1; i < caughtExceptionTypes.Length; i++)
{
return catches
.Select(clause =>
{
var declarationType = GetDeclarationType(clause);
return declarationType != null
? semanticModel.GetTypeInfo(declarationType).Type as INamedTypeSymbol
: null;
})
.ToList();
var nextCatchType = caughtExceptionTypes[i];
if (nextCatchType is null || currentType.DerivesOrImplements(nextCatchType))
{
return redundantCatches.Contains(catches[i]);
}
}
return true;
}

private INamedTypeSymbol[] ComputeExceptionTypes(IEnumerable<TCatchClause> catches, SemanticModel model) =>
catches.Select(x => DeclarationType(x) is { } declarationType ? model.GetTypeInfo(declarationType).Type as INamedTypeSymbol : null).ToArray();
}
34 changes: 16 additions & 18 deletions analyzers/src/SonarAnalyzer.VisualBasic/Rules/CatchRethrow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,27 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Rules.VisualBasic
namespace SonarAnalyzer.Rules.VisualBasic;

[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
public sealed class CatchRethrow : CatchRethrowBase<SyntaxKind, CatchBlockSyntax>
{
[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
public sealed class CatchRethrow : CatchRethrowBase<CatchBlockSyntax>
{
private static readonly SyntaxList<StatementSyntax> ThrowBlock = new SyntaxList<StatementSyntax>().Add(SyntaxFactory.ThrowStatement());
private static readonly SyntaxList<ThrowStatementSyntax> ThrowBlock = SyntaxFactory.List([SyntaxFactory.ThrowStatement()]);

protected override DiagnosticDescriptor Rule { get; } =
DescriptorFactory.Create(DiagnosticId, MessageFormat);
protected override ILanguageFacade<SyntaxKind> Language => VisualBasicFacade.Instance;

protected override bool ContainsOnlyThrow(CatchBlockSyntax currentCatch) =>
VisualBasicEquivalenceChecker.AreEquivalent(currentCatch.Statements, ThrowBlock);
protected override bool ContainsOnlyThrow(CatchBlockSyntax currentCatch) =>
VisualBasicEquivalenceChecker.AreEquivalent(currentCatch.Statements, ThrowBlock);

protected override IReadOnlyList<CatchBlockSyntax> GetCatches(SyntaxNode syntaxNode) =>
((TryBlockSyntax)syntaxNode).CatchBlocks;
protected override CatchBlockSyntax[] AllCatches(SyntaxNode node) =>
((TryBlockSyntax)node).CatchBlocks.ToArray();

protected override SyntaxNode GetDeclarationType(CatchBlockSyntax catchClause) =>
catchClause.CatchStatement?.AsClause?.Type;
protected override SyntaxNode DeclarationType(CatchBlockSyntax catchClause) =>
catchClause.CatchStatement?.AsClause?.Type;

protected override bool HasFilter(CatchBlockSyntax catchClause) =>
catchClause.CatchStatement?.WhenClause != null;
protected override bool HasFilter(CatchBlockSyntax catchClause) =>
catchClause.CatchStatement?.WhenClause is not null;

protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(RaiseOnInvalidCatch, SyntaxKind.TryBlock);
}
protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterNodeAction(RaiseOnInvalidCatch, SyntaxKind.TryBlock);
}
35 changes: 17 additions & 18 deletions analyzers/tests/SonarAnalyzer.Test/Rules/CatchRethrowTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,25 @@
using CS = SonarAnalyzer.Rules.CSharp;
using VB = SonarAnalyzer.Rules.VisualBasic;

namespace SonarAnalyzer.Test.Rules
namespace SonarAnalyzer.Test.Rules;

[TestClass]
public class CatchRethrowTest
{
[TestClass]
public class CatchRethrowTest
{
private readonly VerifierBuilder builderCS = new VerifierBuilder<CS.CatchRethrow>();
private readonly VerifierBuilder builderCS = new VerifierBuilder<CS.CatchRethrow>();

[TestMethod]
public void CatchRethrow() =>
builderCS.AddPaths("CatchRethrow.cs").Verify();
[TestMethod]
public void CatchRethrow() =>
builderCS.AddPaths("CatchRethrow.cs").Verify();

[TestMethod]
public void CatchRethrow_CodeFix() =>
builderCS.AddPaths("CatchRethrow.cs")
.WithCodeFix<CS.CatchRethrowCodeFix>()
.WithCodeFixedPaths("CatchRethrow.Fixed.cs")
.VerifyCodeFix();
[TestMethod]
public void CatchRethrow_CodeFix() =>
builderCS.AddPaths("CatchRethrow.cs")
.WithCodeFix<CS.CatchRethrowCodeFix>()
.WithCodeFixedPaths("CatchRethrow.Fixed.cs")
.VerifyCodeFix();

[TestMethod]
public void CatchRethrow_VB() =>
new VerifierBuilder<VB.CatchRethrow>().AddPaths("CatchRethrow.vb").Verify();
}
[TestMethod]
public void CatchRethrow_VB() =>
new VerifierBuilder<VB.CatchRethrow>().AddPaths("CatchRethrow.vb").Verify();
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,14 @@ public class Repro8199

public void CatchWithFilter()
{
SomeMethod();
try
{
SomeMethod();
}
catch (Exception ex) when (LogException(ex))
{
throw;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public void CatchWithFilter()
{
SomeMethod();
}
catch (Exception ex) when (LogException(ex)) // Noncompliant - FP
catch (Exception ex) when (LogException(ex))
{
throw;
}
Expand Down
4 changes: 2 additions & 2 deletions analyzers/tests/SonarAnalyzer.Test/TestCases/CatchRethrow.vb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Namespace Tests.TestCases

Try
doSomething()
Catch exc As ArgumentException When True 'Noncompliant
Catch exc As ArgumentException When True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Catch exc As ArgumentException When True
Catch exc As ArgumentException When True ' FN

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll consider the True just a scaffolding noise. It's not a SE rule to determine if the expression is always true

Throw
End Try

Expand Down Expand Up @@ -110,7 +110,7 @@ Namespace Tests.TestCases
Public Sub CatchWithFilter()
Try
SomeMethod()
Catch ex As Exception When LogException(ex) ' Noncompliant - FP
Catch ex As Exception When LogException(ex)
Throw
End Try
End Sub
Expand Down
Loading