From d6e1dd777036bfb47649b01c8c6e272f67380491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 15:24:13 +0200 Subject: [PATCH 1/3] feat: add non-strict exception classification --- CHANGELOG.md | 2 ++ CheckedExceptions.Tests/AnazylerConfigTest.cs | 27 +++++++++++++++++++ .../CSharpAnalyzerVerifier.cs | 7 +++++ .../CheckedExceptions.settings.json | 1 + ...sAnalyzerTests.ThrowsExceptionCatchRest.cs | 1 + .../CodeFixes/CSharpCodeFixVerifier.cs | 5 ++++ CheckedExceptions/AnalyzerSettings.cs | 4 +++ .../CheckedExceptionsAnalyzer.Analysis.cs | 3 ++- ...eptionsAnalyzer.ExceptionClassification.cs | 2 +- .../CheckedExceptionsAnalyzer.Throw.cs | 3 ++- .../CheckedExceptionsAnalyzer.cs | 6 ++--- README.md | 8 +++--- SampleProject/CheckedExceptions.settings.json | 3 ++- Test/CheckedExceptions.settings.json | 3 ++- default-settings.json | 1 + docs/analyzer-specification.md | 5 ++-- docs/exception-handling.md | 10 ++++--- schemas/settings-schema.json | 12 +++++++-- 18 files changed, 84 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 674f5c2..2c95525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide comprehensive baseline exception classifications in `default-settings.json` +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Introduce `NonStrict` exception classification and `defaultExceptionClassification` setting ### Changed - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Replace `ignoredExceptions` and `informationalExceptions` with explicit `exceptions` classification map - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Document explicit exception taxonomy and strict default for unlisted types in README and docs +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Unlisted exceptions now default to `NonStrict` producing low-severity diagnostics ### Deprecated diff --git a/CheckedExceptions.Tests/AnazylerConfigTest.cs b/CheckedExceptions.Tests/AnazylerConfigTest.cs index 926395e..bf86b73 100644 --- a/CheckedExceptions.Tests/AnazylerConfigTest.cs +++ b/CheckedExceptions.Tests/AnazylerConfigTest.cs @@ -78,4 +78,31 @@ public void TestMethod2() await Verifier.VerifyAnalyzerAsync2(test, expected1, expected2); } + + [Fact(DisplayName = "Unlisted exception defaults to NonStrict")] + public async Task UnlistedExceptionDefaultsToNonStrict_ShouldReportInfoDiagnostic() + { + var test = /* lang=c#-test */ """ + using System; + + public class TestClass + { + public void TestMethod() + { + throw new InvalidOperationException(); + } + } + """; + + var expected = Verifier.Informational("InvalidOperationException") + .WithSpan(7, 9, 7, 47); + + await Verifier.VerifyAnalyzerAsync(test, t => + { + t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", "{}")); + var allDiagnostics = CheckedExceptionsAnalyzer.AllDiagnosticsIds; + t.DisabledDiagnostics.AddRange(allDiagnostics.Except(new[] { expected.Id })); + t.ExpectedDiagnostics.Add(expected); + }); + } } \ No newline at end of file diff --git a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs index 6c706c6..3138a4c 100644 --- a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs +++ b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Testing; @@ -92,6 +93,7 @@ await VerifyAnalyzerAsync(source, (test) => test.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """" { + "defaultExceptionClassification": "Strict", "ignoredExceptions": [ "System.NotImplementedException" ], @@ -127,6 +129,11 @@ public static Task VerifyAnalyzerAsync([StringSyntax("c#-test")] string source, setup?.Invoke(test); + if (!test.TestState.AdditionalFiles.Any(f => f.filename == "CheckedExceptions.settings.json")) + { + test.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", "{\n \"defaultExceptionClassification\": \"Strict\"\n}")); + } + return test.RunAsync(); } diff --git a/CheckedExceptions.Tests/CheckedExceptions.settings.json b/CheckedExceptions.Tests/CheckedExceptions.settings.json index 2dd2ac4..6e8aaf6 100644 --- a/CheckedExceptions.Tests/CheckedExceptions.settings.json +++ b/CheckedExceptions.Tests/CheckedExceptions.settings.json @@ -1,4 +1,5 @@ { + "defaultExceptionClassification": "Strict", "exceptions": { "System.ArgumentNullException": "Ignored", "System.NotImplementedException": "Informational", diff --git a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs index 1a8fdc6..a495d19 100644 --- a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs +++ b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs @@ -58,6 +58,7 @@ await Verifier.VerifyAnalyzerAsync(test, t => { t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """ { + "defaultExceptionClassification": "Strict", "exceptions": {}, "treatThrowsExceptionAsCatchRest": true } diff --git a/CheckedExceptions.Tests/CodeFixes/CSharpCodeFixVerifier.cs b/CheckedExceptions.Tests/CodeFixes/CSharpCodeFixVerifier.cs index ebc3bb2..3cbda4e 100644 --- a/CheckedExceptions.Tests/CodeFixes/CSharpCodeFixVerifier.cs +++ b/CheckedExceptions.Tests/CodeFixes/CSharpCodeFixVerifier.cs @@ -69,6 +69,11 @@ public static Task VerifyCodeFixAsync([StringSyntax("c#-test")] string source, I setup?.Invoke(test); + if (!test.TestState.AdditionalFiles.Any(f => f.filename == "CheckedExceptions.settings.json")) + { + test.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", "{\n \"defaultExceptionClassification\": \"Strict\"\n}")); + } + return test.RunAsync(); } diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index 4aac697..3d55dd9 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -64,6 +64,9 @@ public partial class AnalyzerSettings [JsonIgnore] internal bool TreatThrowsExceptionAsCatchRestEnabled => TreatThrowsExceptionAsCatchRest; + [JsonPropertyName("defaultExceptionClassification")] + public ExceptionClassification DefaultExceptionClassification { get; set; } = ExceptionClassification.NonStrict; + [JsonPropertyName("exceptions")] public IDictionary Exceptions { get; set; } = new Dictionary(); @@ -111,5 +114,6 @@ public enum ExceptionClassification { Ignored, Informational, + NonStrict, Strict } \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs index e5faa09..398f886 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs @@ -54,7 +54,8 @@ private static void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext contex { continue; } - else if (classification is ExceptionClassification.Informational) + else if (classification is ExceptionClassification.Informational + || classification is ExceptionClassification.NonStrict) { var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); context.ReportDiagnostic(diagnostic); diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs index 62a1b71..275cdd2 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs @@ -15,7 +15,7 @@ public static ExceptionClassification GetExceptionClassification( return classification; } - return ExceptionClassification.Strict; + return settings.DefaultExceptionClassification; } public static bool ShouldIncludeException( diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs index 97a9f00..ad943d8 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs @@ -45,7 +45,8 @@ private static void AnalyzeExceptionThrowingNode( { return; } - else if (classification is ExceptionClassification.Informational) + else if (classification is ExceptionClassification.Informational + || classification is ExceptionClassification.NonStrict) { var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); reportDiagnostic(diagnostic); diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.cs index e53e105..3de1bd6 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -51,12 +51,12 @@ public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor RuleIgnoredException = new DiagnosticDescriptor( DiagnosticIdIgnoredException, - "Ignored exception may cause runtime issues", - "Exception '{0}' is ignored by configuration but may cause runtime issues if unhandled", + "Non-strict exception may cause runtime issues", + "Exception '{0}' is not strictly enforced and may cause runtime issues if unhandled", "Usage", DiagnosticSeverity.Info, isEnabledByDefault: true, - description: "Informs about exceptions excluded from analysis but which may still propagate at runtime if not properly handled."); + description: "Informs about exceptions that are not strictly enforced but which may still propagate at runtime if not properly handled."); private static readonly DiagnosticDescriptor RuleGeneralThrow = new( DiagnosticIdGeneralThrow, diff --git a/README.md b/README.md index 7958ed8..7f1ce78 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,18 @@ dotnet_diagnostic.THROW003.severity = warning A baseline template is available in `default-settings.json`. -The analyzer reads a single `exceptions` dictionary that explicitly classifies each exception type as `Ignored`, `Informational`, or `Strict`. Any exception not listed defaults to `Strict`, so an unclassified throw will trigger a diagnostic unless it's caught or declared with `[Throws]`. +The analyzer reads a single `exceptions` dictionary that explicitly classifies each exception type as `Ignored`, `Informational`, `NonStrict`, or `Strict`. Any exception not listed defaults to `NonStrict`, so an unclassified throw will trigger a low-severity diagnostic but won't require `[Throws]` or a `catch`. Add `CheckedExceptions.settings.json`: ```json { + // Default classification for exceptions not listed (default: NonStrict) + "defaultExceptionClassification": "NonStrict", "exceptions": { "System.ArgumentNullException": "Ignored", "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational", + "System.TimeoutException": "NonStrict", "System.Exception": "Strict" }, @@ -194,7 +196,7 @@ Register in `.csproj`: | ID | Message | |------------|-------------------------------------------------------------------------| | `THROW001` | ❗ Unhandled exception: must be caught or declared | -| `THROW002` | ℹ️ Ignored exception may cause runtime issues | +| `THROW002` | ℹ️ Non-strict exception may cause runtime issues | | `THROW003` | 🚫 Avoid declaring exception type `System.Exception` | | `THROW004` | 🚫 Avoid throwing exception base type `System.Exception` | | `THROW005` | 🔁 Duplicate declarations of the same exception type in `[Throws]` | diff --git a/SampleProject/CheckedExceptions.settings.json b/SampleProject/CheckedExceptions.settings.json index eaeb61b..b7c9add 100644 --- a/SampleProject/CheckedExceptions.settings.json +++ b/SampleProject/CheckedExceptions.settings.json @@ -1,8 +1,9 @@ { + "defaultExceptionClassification": "NonStrict", "exceptions": { "System.NotImplementedException": "Informational", "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational" + "System.TimeoutException": "NonStrict" }, "disableXmlDocInterop": false, "disableLinqSupport": false, diff --git a/Test/CheckedExceptions.settings.json b/Test/CheckedExceptions.settings.json index eaeb61b..b7c9add 100644 --- a/Test/CheckedExceptions.settings.json +++ b/Test/CheckedExceptions.settings.json @@ -1,8 +1,9 @@ { + "defaultExceptionClassification": "NonStrict", "exceptions": { "System.NotImplementedException": "Informational", "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational" + "System.TimeoutException": "NonStrict" }, "disableXmlDocInterop": false, "disableLinqSupport": false, diff --git a/default-settings.json b/default-settings.json index 0b9afc0..5c015bc 100644 --- a/default-settings.json +++ b/default-settings.json @@ -1,4 +1,5 @@ { + "defaultExceptionClassification": "NonStrict", "exceptions": { "System.ArgumentNullException": "Ignored", "System.ArgumentOutOfRangeException": "Ignored", diff --git a/docs/analyzer-specification.md b/docs/analyzer-specification.md index 385a939..99fb5b9 100644 --- a/docs/analyzer-specification.md +++ b/docs/analyzer-specification.md @@ -337,13 +337,14 @@ public int Value { set => throw new InvalidOperationException(); } ## Exception classification (Core analysis) -`CheckedExceptions.settings.json` contains an explicit `exceptions` map that classifies each exception type as `Ignored`, `Informational`, or `Strict`. +`CheckedExceptions.settings.json` contains an explicit `exceptions` map that classifies each exception type as `Ignored`, `Informational`, `NonStrict`, or `Strict`. - **Ignored** – no diagnostics are produced. - **Informational** – diagnostics are reported but `[Throws]` is not required. +- **NonStrict** – diagnostics are reported at low severity without requiring `[Throws]` or a `catch` block. - **Strict** – exceptions must be caught or declared. -Any exception not listed defaults to **Strict**, so unclassified types will trigger `THROW001` until they are handled or declared. +Any exception not listed defaults to **NonStrict**, so unclassified types will trigger `THROW002` until they are handled or declared. > **Migration note:** Legacy `ignoredExceptions` and `informationalExceptions` properties are still processed for backward compatibility, but they are deprecated and translated into entries in the `exceptions` map. diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 2d1b0d6..d3276cf 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -49,7 +49,7 @@ The **CheckedExceptions Analyzer** serves three purposes: 2. **Help you propagate exceptions explicitly** by requiring `[Throws]` declarations when you choose not to handle them locally. 3. **Provide control flow analysis** that highlights unreachable code and redundant catch blocks. -To keep this analysis deterministic, configuration is an explicit taxonomy. Each entry in `CheckedExceptions.settings.json` maps an exception type to `Ignored`, `Informational`, or `Strict`. Any type not present defaults to **Strict**, meaning uncaught, undeclared exceptions will trigger diagnostics until you catch them or annotate them with `[Throws]`. +To keep this analysis deterministic, configuration is an explicit taxonomy. Each entry in `CheckedExceptions.settings.json` maps an exception type to `Ignored`, `Informational`, `NonStrict`, or `Strict`. Any type not present defaults to **NonStrict**, meaning unclassified exceptions raise low-severity diagnostics but do not require `[Throws]` or a `catch` block. --- @@ -494,11 +494,12 @@ Create a `CheckedExceptions.settings.json` file with the following structure: ```json { + "defaultExceptionClassification": "NonStrict", "exceptions": { "System.ArgumentNullException": "Ignored", "System.NotImplementedException": "Informational", - "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational" + "System.IO.IOException": "Strict", + "System.TimeoutException": "NonStrict" } } ``` @@ -522,9 +523,10 @@ Add the settings file to your `.csproj`: - **`Ignored`**: Exceptions with this classification are completely ignored—no diagnostics or error reports will be generated. - **`Informational`**: Exceptions generate informational diagnostics but do not require `[Throws]` declarations. +- **`NonStrict`**: Exceptions generate low-severity diagnostics but do not require `[Throws]` declarations or a `catch` block. - **`Strict`**: Exceptions must be handled or declared; missing `[Throws]` results in warnings. -Any exception type that doesn't appear in the `exceptions` map defaults to **Strict**. +Any exception type that doesn't appear in the `exceptions` map defaults to **NonStrict**. ## Performance Considerations diff --git a/schemas/settings-schema.json b/schemas/settings-schema.json index 5670418..84c9805 100644 --- a/schemas/settings-schema.json +++ b/schemas/settings-schema.json @@ -45,7 +45,13 @@ "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." + "description": "Treat [Throws(typeof(Exception))] as a catch-all for remaining exceptions and suppress hierarchy redundancy checks; the base-type diagnostic remains active." + }, + "defaultExceptionClassification": { + "type": "string", + "enum": ["Ignored", "Informational", "NonStrict", "Strict"], + "description": "Classification applied to exceptions not listed in 'exceptions'.", + "default": "NonStrict" }, "ignoredExceptions": { "type": "array", @@ -66,6 +72,7 @@ "enum": [ "Ignored", "Informational", + "NonStrict", "Strict" ] }, @@ -74,4 +81,5 @@ }, "required": [], "additionalProperties": false -} \ No newline at end of file +} + From b0070c5ab532239c04dc2a066067e02c9d70d7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 15:33:10 +0200 Subject: [PATCH 2/3] Document NonStrict handling and add tests --- CHANGELOG.md | 1 + ...ptionsAnalyzerTests.NonStrictRedundancy.cs | 105 ++++++++++++++++++ README.md | 2 +- docs/exception-handling.md | 4 +- 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.NonStrictRedundancy.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c95525..6e288a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide comprehensive baseline exception classifications in `default-settings.json` - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Introduce `NonStrict` exception classification and `defaultExceptionClassification` setting +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Document `NonStrict` declaration/catch behavior and add regression tests ### Changed diff --git a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.NonStrictRedundancy.cs b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.NonStrictRedundancy.cs new file mode 100644 index 0000000..3a06bc9 --- /dev/null +++ b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.NonStrictRedundancy.cs @@ -0,0 +1,105 @@ +using Microsoft.CodeAnalysis.Testing; + +namespace Sundstrom.CheckedExceptions.Tests; + +using Verifier = CSharpAnalyzerVerifier; + +public partial class CheckedExceptionsAnalyzerTests +{ + [Fact] + public async Task DeclaringNonStrictException_ShouldNotReportRedundantDeclaration() + { + var test = /* lang=c#-test */ """ + using System; + + public class TestClass + { + [Throws(typeof(InvalidOperationException))] + public void Test() + { + throw new InvalidOperationException(); + } + } + """; + + await Verifier.VerifyAnalyzerAsync(test, t => + { + t.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration); + t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdIgnoredException); + t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """ + { + "defaultExceptionClassification": "Strict", + "exceptions": {"System.InvalidOperationException": "NonStrict"} + } + """)); + }); + } + + [Fact] + public async Task CatchingNonStrictException_TypedCatch_ShouldNotReportRedundantCatch() + { + var test = /* lang=c#-test */ """ + using System; + + public class TestClass + { + public void Test() + { + try + { + throw new InvalidOperationException(); + } + catch (InvalidOperationException) + { + } + } + } + """; + + await Verifier.VerifyAnalyzerAsync(test, t => + { + t.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause); + t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdIgnoredException); + t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """ + { + "defaultExceptionClassification": "Strict", + "exceptions": {"System.InvalidOperationException": "NonStrict"} + } + """)); + }); + } + + [Fact] + public async Task CatchingNonStrictException_CatchAll_ShouldNotReportRedundantCatchAll() + { + var test = /* lang=c#-test */ """ + using System; + + public class TestClass + { + public void Test() + { + try + { + throw new InvalidOperationException(); + } + catch + { + } + } + } + """; + + await Verifier.VerifyAnalyzerAsync(test, t => + { + t.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantCatchAllClause); + t.DisabledDiagnostics.Add(CheckedExceptionsAnalyzer.DiagnosticIdIgnoredException); + t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """ + { + "defaultExceptionClassification": "Strict", + "exceptions": {"System.InvalidOperationException": "NonStrict"} + } + """)); + }); + } +} diff --git a/README.md b/README.md index 7f1ce78..6aaa270 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ dotnet_diagnostic.THROW003.severity = warning A baseline template is available in `default-settings.json`. -The analyzer reads a single `exceptions` dictionary that explicitly classifies each exception type as `Ignored`, `Informational`, `NonStrict`, or `Strict`. Any exception not listed defaults to `NonStrict`, so an unclassified throw will trigger a low-severity diagnostic but won't require `[Throws]` or a `catch`. +The analyzer reads a single `exceptions` dictionary that explicitly classifies each exception type as `Ignored`, `Informational`, `NonStrict`, or `Strict`. Any exception not listed defaults to `NonStrict`, so an unclassified throw will trigger a low-severity diagnostic but won't require `[Throws]` or a `catch`. `NonStrict` exceptions remain part of analysis—you may still declare or catch them, and doing so isn't considered redundant. Only exceptions classified as `Ignored` are completely filtered out. Add `CheckedExceptions.settings.json`: diff --git a/docs/exception-handling.md b/docs/exception-handling.md index d3276cf..1d5b3e5 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -523,10 +523,10 @@ Add the settings file to your `.csproj`: - **`Ignored`**: Exceptions with this classification are completely ignored—no diagnostics or error reports will be generated. - **`Informational`**: Exceptions generate informational diagnostics but do not require `[Throws]` declarations. -- **`NonStrict`**: Exceptions generate low-severity diagnostics but do not require `[Throws]` declarations or a `catch` block. +- **`NonStrict`**: Exceptions generate low-severity diagnostics but do not require `[Throws]` declarations or a `catch` block. You may still declare or catch them, and doing so isn't considered redundant. - **`Strict`**: Exceptions must be handled or declared; missing `[Throws]` results in warnings. -Any exception type that doesn't appear in the `exceptions` map defaults to **NonStrict**. +Only exceptions classified as `Ignored` are filtered out entirely. Any exception type that doesn't appear in the `exceptions` map defaults to **NonStrict**, remaining part of the analysis. ## Performance Considerations From 8180c204716eda9a3a93babb2b2b382c02bfbb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 15:39:27 +0200 Subject: [PATCH 3/3] docs: add configuration guide --- CHANGELOG.md | 1 + README.md | 2 +- docs/configuration-guide.md | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 docs/configuration-guide.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e288a4..5838b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide comprehensive baseline exception classifications in `default-settings.json` - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Introduce `NonStrict` exception classification and `defaultExceptionClassification` setting - PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Document `NonStrict` declaration/catch behavior and add regression tests +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Add configuration guide for selecting default exception classification ### Changed diff --git a/README.md b/README.md index 6aaa270..bd9b955 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ dotnet_diagnostic.THROW003.severity = warning ### JSON Settings -A baseline template is available in `default-settings.json`. +A baseline template is available in `default-settings.json`. For guidance on choosing the right default classification for your project, see the [configuration guide](docs/configuration-guide.md). The analyzer reads a single `exceptions` dictionary that explicitly classifies each exception type as `Ignored`, `Informational`, `NonStrict`, or `Strict`. Any exception not listed defaults to `NonStrict`, so an unclassified throw will trigger a low-severity diagnostic but won't require `[Throws]` or a `catch`. `NonStrict` exceptions remain part of analysis—you may still declare or catch them, and doing so isn't considered redundant. Only exceptions classified as `Ignored` are completely filtered out. diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md new file mode 100644 index 0000000..bae6e43 --- /dev/null +++ b/docs/configuration-guide.md @@ -0,0 +1,21 @@ +# Configuration Guide + +Choosing the right default classification determines how the analyzer treats exceptions that are not explicitly listed in the `exceptions` map. + +## Default strategies + +### `Ignored` +Use when you only want to analyze exceptions that are explicitly classified. Unlisted exceptions will be ignored entirely. + +### `NonStrict` +The recommended starting point. Unlisted exceptions remain part of analysis and raise low-severity diagnostics (`THROW002` style) but do not require `[Throws]` declarations or `catch` blocks. + +### `Strict` +Use for maximum enforcement. Any unlisted exception must be either declared with `[Throws]` or handled with a `catch` block; otherwise a high-severity diagnostic is reported. + +## Tips +- Start with `NonStrict` to discover thrown exceptions without breaking builds. +- Move to `Strict` once you have classified all expected exceptions and want strong enforcement. +- Prefer `Ignored` only when you explicitly list every exception of interest or when onboarding gradually. + +You can change the default via the `defaultExceptionClassification` setting and override individual types in the `exceptions` map.