Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- PR [#301](https://github.com/marinasundstrom/CheckedExceptions/pull/301) Allow treating `Exception` in `[Throws]` as a catch-all via `treatThrowsExceptionAsCatchRest` setting (base-type diagnostic unchanged)

## [2.2.3] - 2025-08-24

### Fixed
Expand Down
1 change: 1 addition & 0 deletions CheckedExceptions.Tests/CheckedExceptions.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"disableLinqImplicitlyDeclaredExceptions": false,
"disableControlFlowAnalysis": false,
"enableLegacyRedundancyChecks": false,
"treatThrowsExceptionAsCatchRest": false,
"disableBaseExceptionDeclaredDiagnostic": false,
"disableBaseExceptionThrownDiagnostic": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;

namespace Sundstrom.CheckedExceptions.Tests;

using Verifier = CSharpAnalyzerVerifier<CheckedExceptionsAnalyzer, DefaultVerifier>;

public partial class CheckedExceptionsAnalyzerTests
{
[Fact]
public async Task DeclaringExceptionWithSpecific_ShouldReportRedundantDiagnosticByDefault()
{
var test = /* lang=c#-test */ """
using System;

public class TestClass
{
[Throws(typeof(InvalidOperationException), typeof(Exception))]
public void TestMethod()
{
// Throws "FormatException" and "OverflowException"
var x = int.Parse("42");
throw new InvalidOperationException();
}
}
""";

var expectedRedundant = Verifier.RedundantExceptionDeclarationBySuperType("Exception")
.WithSpan(5, 20, 5, 45);

await Verifier.VerifyAnalyzerAsync(test, expectedRedundant);
}

[Fact]
public async Task DeclaringExceptionWithSpecific_TreatAsCatchRest_ShouldReportBaseDiagnostic()
{
var test = /* lang=c#-test */ """
using System;

public class TestClass
{
[Throws(typeof(InvalidOperationException), typeof(Exception))]
public void TestMethod()
{
// Throws "FormatException" and "OverflowException"
var x = int.Parse("42");
throw new InvalidOperationException();
}
}
""";

var expected = Verifier.AvoidDeclaringTypeException()
.WithSpan(5, 55, 5, 64);

await Verifier.VerifyAnalyzerAsync(test, t =>
{
t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """
{
"ignoredExceptions": [],
"informationalExceptions": {},
"treatThrowsExceptionAsCatchRest": true
}
"""));

var allDiagnostics = CheckedExceptionsAnalyzer.AllDiagnosticsIds;
t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause);
t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration);
t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdUnreachableCode);
t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdUnreachableCodeHidden);
t.DisabledDiagnostics.AddRange(allDiagnostics.Except(new[] { expected.Id }));

t.ExpectedDiagnostics.Add(expected);
});
}
}
6 changes: 6 additions & 0 deletions CheckedExceptions/AnalyzerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public partial class AnalyzerSettings
[JsonIgnore]
internal bool BaseExceptionThrownDiagnosticEnabled => !DisableBaseExceptionThrownDiagnostic;

[JsonPropertyName("treatThrowsExceptionAsCatchRest")]
public bool TreatThrowsExceptionAsCatchRest { get; set; } = false;

[JsonIgnore]
internal bool TreatThrowsExceptionAsCatchRestEnabled => TreatThrowsExceptionAsCatchRest;

[JsonPropertyName("ignoredExceptions")]
public IEnumerable<string> IgnoredExceptions { get; set; } = new List<string>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading;

using Microsoft.CodeAnalysis;
Expand All @@ -13,6 +14,7 @@ private static void CheckForRedundantThrowsHandledByDeclaredSuperClass(
ThrowsContext context)
{
var semanticModel = context.SemanticModel;
var settings = GetAnalyzerSettings(context.Options);
var declaredTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
var typeToExprMap = new Dictionary<INamedTypeSymbol, TypeOfExpressionSyntax>(SymbolEqualityComparer.Default);

Expand All @@ -34,6 +36,18 @@ private static void CheckForRedundantThrowsHandledByDeclaredSuperClass(
}
}

if (settings.TreatThrowsExceptionAsCatchRestEnabled)
{
foreach (var type in declaredTypes.ToArray())
{
if (type.Name == "Exception" && type.ContainingNamespace?.ToDisplayString() == "System")
{
declaredTypes.Remove(type);
typeToExprMap.Remove(type);
}
}
}

foreach (var type in declaredTypes)
{
foreach (var otherType in declaredTypes)
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ Add `CheckedExceptions.settings.json`:
// If true, basic redundancy checks are available when control flow analysis is disabled (default: false).
"enableLegacyRedundancyChecks": false,

// If true, declaring [Throws(typeof(Exception))] acts as a catch-all and suppresses hierarchy redundancy checks; the base-type
// warning (THROW003) remains unless "disableBaseExceptionDeclaredDiagnostic" is set (default: false).
"treatThrowsExceptionAsCatchRest": false,

// If true, the analyzer will not warn about declaring base type Exception with [Throws] (default: false).
"disableBaseExceptionDeclaredDiagnostic": false,

Expand Down
1 change: 1 addition & 0 deletions SampleProject/CheckedExceptions.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"disableLinqImplicitlyDeclaredExceptions": false,
"disableControlFlowAnalysis": false,
"enableLegacyRedundancyChecks": false,
"treatThrowsExceptionAsCatchRest": false,
"disableBaseExceptionDeclaredDiagnostic": false,
"disableBaseExceptionThrownDiagnostic": false
}
1 change: 1 addition & 0 deletions Test/CheckedExceptions.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"disableLinqImplicitlyDeclaredExceptions": false,
"disableControlFlowAnalysis": false,
"enableLegacyRedundancyChecks": false,
"treatThrowsExceptionAsCatchRest": false,
"disableBaseExceptionDeclaredDiagnostic": false,
"disableBaseExceptionThrownDiagnostic": false
}
12 changes: 12 additions & 0 deletions docs/analyzer-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Control flow analysis is also used to determine whether declarations are truly n
* **Duplicate declarations** → **`THROW005`**

* **Already covered by base type** → **`THROW008`**
(suppressed when `treatThrowsExceptionAsCatchRest` is enabled and the base type is `System.Exception`)

### Invalid placement (Core analysis)

Expand Down Expand Up @@ -471,6 +472,17 @@ These options control whether to warn about the usage of base type `Exception`.
> If another analyzer is used, it might warn about the use of base type `Exceptions` instead.


### Treat `[Throws(typeof(Exception))]` as catch-all

Allows `[Throws(typeof(Exception))]` to act as a catch-all for undeclared exceptions and suppresses diagnostic `THROW008` for redundant declarations covered by `System.Exception`. The base-type declaration diagnostic (`THROW003`) remains active and can be disabled via `disableBaseExceptionDeclaredDiagnostic`.

```json
{
"treatThrowsExceptionAsCatchRest": true
}
```


### Disable LINQ support

This option controls whether all analysis on LINQ constructs should be disabled.
Expand Down
5 changes: 5 additions & 0 deletions schemas/settings-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
"default": false,
"description": "Indicates whether the analyzer should not warn when throwing base type 'Exception'."
},
"treatThrowsExceptionAsCatchRest": {
"type": "boolean",
"default": false,
"description": "Treat [Throws(typeof(Exception))] as a catch-all for remaining exceptions and suppress hierarchy redundancy checks; the base-type diagnostic remains active."
},
"ignoredExceptions": {
"type": "array",
"items": {
Expand Down
Loading