From 459561bf8469adb5f8b0ef9acc77aa8e917969e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 14:12:30 +0200 Subject: [PATCH 1/4] Remove obsolete ignored exceptions logic --- CHANGELOG.md | 5 + CheckedExceptions.Tests/AnazylerConfigTest.cs | 16 +-- .../CSharpAnalyzerVerifier.cs | 10 +- .../CheckedExceptions.settings.json | 12 +-- ...sAnalyzerTests.ThrowsExceptionCatchRest.cs | 3 +- CheckedExceptions/AnalyzerSettings.cs | 15 ++- .../CheckedExceptionsAnalyzer.Analysis.cs | 35 +++--- ...eptionsAnalyzer.ExceptionClassification.cs | 26 +++++ ...kedExceptionsAnalyzer.IgnoredExceptions.cs | 100 ------------------ .../CheckedExceptionsAnalyzer.Throw.cs | 17 ++- README.md | 18 ++-- SampleProject/CheckedExceptions.settings.json | 9 +- SampleProject/SampleProject.csproj | 13 +-- Test/CheckedExceptions.settings.json | 9 +- Test/Test.csproj | 14 +-- default-settings.json | 18 ++++ docs/exception-handling.md | 33 ++---- schemas/settings-schema.json | 23 ++-- 18 files changed, 133 insertions(+), 243 deletions(-) create mode 100644 CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs delete mode 100644 CheckedExceptions/CheckedExceptionsAnalyzer.IgnoredExceptions.cs create mode 100644 default-settings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df8633..129cfb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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) +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide baseline exception classifications in `default-settings.json` + +### Changed + +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Replace `ignoredExceptions` and `informationalExceptions` with explicit `exceptions` classification map ## [2.2.3] - 2025-08-24 diff --git a/CheckedExceptions.Tests/AnazylerConfigTest.cs b/CheckedExceptions.Tests/AnazylerConfigTest.cs index 8cb0508..926395e 100644 --- a/CheckedExceptions.Tests/AnazylerConfigTest.cs +++ b/CheckedExceptions.Tests/AnazylerConfigTest.cs @@ -70,12 +70,12 @@ public void TestMethod2() } """; - var expected1 = Verifier.Informational("IOException") - .WithSpan(9, 21, 9, 39); - - var expected2 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantCatchAllClause) - .WithSpan(11, 9, 11, 14); - - await Verifier.VerifyAnalyzerAsync2(test, expected1, expected2); - } + var expected1 = Verifier.Informational("IOException") + .WithSpan(9, 21, 9, 39); + + var expected2 = Verifier.Informational("IOException") + .WithSpan(13, 13, 13, 19); + + await Verifier.VerifyAnalyzerAsync2(test, expected1, expected2); + } } \ No newline at end of file diff --git a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs index 4faf9e1..9d081e9 100644 --- a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs +++ b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs @@ -92,12 +92,10 @@ await VerifyAnalyzerAsync(source, (test) => test.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """" { - "ignoredExceptions": [ - "System.NotImplementedException" - ], - "informationalExceptions": { - "System.IO.IOException": "Always", - "System.TimeoutException": "Always" + "exceptions": { + "System.NotImplementedException": "Ignored", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational" } } """")); diff --git a/CheckedExceptions.Tests/CheckedExceptions.settings.json b/CheckedExceptions.Tests/CheckedExceptions.settings.json index b404f78..2dd2ac4 100644 --- a/CheckedExceptions.Tests/CheckedExceptions.settings.json +++ b/CheckedExceptions.Tests/CheckedExceptions.settings.json @@ -1,11 +1,9 @@ { - "ignoredExceptions": [ - "System.ArgumentNullException" - ], - "informationalExceptions": { - "System.NotImplementedException": "Propagation", - "System.IO.IOException": "Propagation", - "System.TimeoutException": "Always" + "exceptions": { + "System.ArgumentNullException": "Ignored", + "System.NotImplementedException": "Informational", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational" }, "disableXmlDocInterop": false, "disableLinqSupport": false, diff --git a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs index 786dcc3..1a8fdc6 100644 --- a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs +++ b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.ThrowsExceptionCatchRest.cs @@ -58,8 +58,7 @@ await Verifier.VerifyAnalyzerAsync(test, t => { t.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """ { - "ignoredExceptions": [], - "informationalExceptions": {}, + "exceptions": {}, "treatThrowsExceptionAsCatchRest": true } """)); diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index f79ef86..32f89af 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -64,17 +64,14 @@ public partial class AnalyzerSettings [JsonIgnore] internal bool TreatThrowsExceptionAsCatchRestEnabled => TreatThrowsExceptionAsCatchRest; - [JsonPropertyName("ignoredExceptions")] - public IEnumerable IgnoredExceptions { get; set; } = new List(); - - [JsonPropertyName("informationalExceptions")] - public IDictionary InformationalExceptions { get; set; } = new Dictionary(); + [JsonPropertyName("exceptions")] + public IDictionary Exceptions { get; set; } = new Dictionary(); } [JsonConverter(typeof(JsonStringEnumConverter))] -public enum ExceptionMode +public enum ExceptionClassification { - Throw = 1, - Propagation = 2, - Always = Throw | Propagation + Ignored, + Informational, + Strict } \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs index 7245974..e5faa09 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs @@ -48,22 +48,17 @@ private static void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext contex // For each thrown exception, check if it is handled foreach (var exceptionType in thrownExceptions.Distinct(SymbolEqualityComparer.Default).OfType()) { - var exceptionName = exceptionType.ToDisplayString(); + var classification = GetExceptionClassification(exceptionType, settings); - if (FilterIgnored(settings, exceptionName)) + if (classification is ExceptionClassification.Ignored) { - // Completely ignore this exception continue; } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + else if (classification is ExceptionClassification.Informational) { - if (ShouldIgnore(throwStatement, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); - context.ReportDiagnostic(diagnostic); - continue; - } + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); + context.ReportDiagnostic(diagnostic); + continue; } // ① handled by any typed catch BEFORE the general catch? @@ -168,7 +163,7 @@ private static HashSet CollectExceptionsFromStatement(Statemen var exceptionType = semanticModel.GetTypeInfo(throwStatement.Expression).Type as INamedTypeSymbol; if (exceptionType is not null) { - if (ShouldIncludeException(exceptionType, throwStatement, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -199,7 +194,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi var exceptionType = semanticModel.GetTypeInfo(throwExpression.Expression).Type as INamedTypeSymbol; if (exceptionType is not null) { - if (ShouldIncludeException(exceptionType, throwExpression, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -225,7 +220,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi foreach (var exceptionType in exceptionTypes) { - if (ShouldIncludeException(exceptionType, invocation, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -250,7 +245,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi foreach (var exceptionType in exceptionTypes) { - if (ShouldIncludeException(exceptionType, invocation, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -282,7 +277,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi foreach (var exceptionType in exceptionTypes) { - if (ShouldIncludeException(exceptionType, objectCreation, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -301,7 +296,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi foreach (var exceptionType in exceptionTypes) { - if (ShouldIncludeException(exceptionType, memberAccess, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -319,7 +314,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi foreach (var exceptionType in exceptionTypes) { - if (ShouldIncludeException(exceptionType, elementAccess, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -339,7 +334,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi { if (exceptionType is not null) { - if (ShouldIncludeException(exceptionType, identifier, settings)) + if (ShouldIncludeException(exceptionType, settings)) { exceptions.Add(exceptionType); } @@ -361,7 +356,7 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi if (invalidCastException is not null) { - if (ShouldIncludeException(invalidCastException, castExpression, settings)) + if (ShouldIncludeException(invalidCastException, settings)) { exceptions.Add(invalidCastException); } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs new file mode 100644 index 0000000..62a1b71 --- /dev/null +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.ExceptionClassification.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; + +namespace Sundstrom.CheckedExceptions; + +partial class CheckedExceptionsAnalyzer +{ + public static ExceptionClassification GetExceptionClassification( + INamedTypeSymbol exceptionType, + AnalyzerSettings settings) + { + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.Exceptions.TryGetValue(exceptionName, out var classification)) + { + return classification; + } + + return ExceptionClassification.Strict; + } + + public static bool ShouldIncludeException( + INamedTypeSymbol exceptionType, + AnalyzerSettings settings) + => GetExceptionClassification(exceptionType, settings) != ExceptionClassification.Ignored; +} + diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.IgnoredExceptions.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.IgnoredExceptions.cs deleted file mode 100644 index 570655a..0000000 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.IgnoredExceptions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Sundstrom.CheckedExceptions; - -partial class CheckedExceptionsAnalyzer -{ - private static bool ShouldIgnore(SyntaxNode node, ExceptionMode mode) - { - if (mode is ExceptionMode.Always) - return true; - - if (mode is ExceptionMode.Throw && node is ThrowStatementSyntax or ThrowExpressionSyntax) - return true; - - if (mode is ExceptionMode.Propagation && node - is MemberAccessExpressionSyntax - or IdentifierNameSyntax - or InvocationExpressionSyntax) - return true; - - return false; - } - - public static bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) - { - var exceptionName = exceptionType.ToDisplayString(); - - if (FilterIgnored(settings, exceptionName)) - { - // Completely ignore this exception - return false; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(node, mode)) - { - return false; - } - } - - return true; - } - - private static bool FilterIgnored(AnalyzerSettings settings, string exceptionName) - { - bool matchedPositive = false; - - // First pass: check negations - foreach (var pattern in settings.IgnoredExceptions) - { - if (string.IsNullOrWhiteSpace(pattern)) - continue; - - if (pattern.StartsWith("!")) - { - var negated = pattern.Substring(1); - if (IsMatch(exceptionName, negated)) - { - // Explicitly not ignored -> wins immediately - return false; - } - } - } - - // Second pass: check positive patterns - foreach (var pattern in settings.IgnoredExceptions) - { - if (string.IsNullOrWhiteSpace(pattern)) - continue; - - if (!pattern.StartsWith("!")) - { - if (IsMatch(exceptionName, pattern)) - { - matchedPositive = true; - } - } - } - - return matchedPositive; - } - - private static bool IsMatch(string exceptionName, string pattern) - { - // Wildcard '*' support - if (pattern.Contains('*')) - { - var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern) - .Replace("\\*", ".*") + "$"; - - return System.Text.RegularExpressions.Regex.IsMatch(exceptionName, regexPattern); - } - - // Exact match - return string.Equals(exceptionName, pattern, StringComparison.Ordinal); - } -} \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs index 2783673..97a9f00 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Throw.cs @@ -39,22 +39,17 @@ private static void AnalyzeExceptionThrowingNode( if (exceptionType is null) return; - var exceptionName = exceptionType.ToDisplayString(); + var classification = GetExceptionClassification(exceptionType, settings); - if (FilterIgnored(settings, exceptionName)) + if (classification is ExceptionClassification.Ignored) { - // Completely ignore this exception return; } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + else if (classification is ExceptionClassification.Informational) { - if (ShouldIgnore(node, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); - reportDiagnostic(diagnostic); - return; - } + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); + reportDiagnostic(diagnostic); + return; } if (settings.BaseExceptionThrownDiagnosticEnabled) diff --git a/README.md b/README.md index 33830b7..248a264 100644 --- a/README.md +++ b/README.md @@ -128,21 +128,17 @@ dotnet_diagnostic.THROW003.severity = warning ### JSON Settings +A baseline template is available in `default-settings.json`. + Add `CheckedExceptions.settings.json`: ```json { - // Exceptions to completely ignore during analysis (Glob pattern). - "ignoredExceptions": [ - "System.*", - "System.ArgumentNullException", - "!System.InvalidOperationException" - ], - - // Exceptions to ignore but still report as informational diagnostics. - "informationalExceptions": { - "System.IO.IOException": "Propagation", - "System.TimeoutException": "Always" + "exceptions": { + "System.ArgumentNullException": "Ignored", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational", + "System.Exception": "Strict" }, // If true, exceptions will not be read from XML documentation (default: false). diff --git a/SampleProject/CheckedExceptions.settings.json b/SampleProject/CheckedExceptions.settings.json index bdc26db..eaeb61b 100644 --- a/SampleProject/CheckedExceptions.settings.json +++ b/SampleProject/CheckedExceptions.settings.json @@ -1,9 +1,8 @@ { - "ignoredExceptions": [], - "informationalExceptions": { - "System.NotImplementedException": "Propagation", - "System.IO.IOException": "Propagation", - "System.TimeoutException": "Always" + "exceptions": { + "System.NotImplementedException": "Informational", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational" }, "disableXmlDocInterop": false, "disableLinqSupport": false, diff --git a/SampleProject/SampleProject.csproj b/SampleProject/SampleProject.csproj index 8fb45aa..970d30c 100644 --- a/SampleProject/SampleProject.csproj +++ b/SampleProject/SampleProject.csproj @@ -10,21 +10,16 @@ - - all - runtime; build; native; contentfiles; analyzers - - - + SetTargetFramework="TargetFramework=netstandard2.0" /> - - \ No newline at end of file + + diff --git a/Test/CheckedExceptions.settings.json b/Test/CheckedExceptions.settings.json index bdc26db..eaeb61b 100644 --- a/Test/CheckedExceptions.settings.json +++ b/Test/CheckedExceptions.settings.json @@ -1,9 +1,8 @@ { - "ignoredExceptions": [], - "informationalExceptions": { - "System.NotImplementedException": "Propagation", - "System.IO.IOException": "Propagation", - "System.TimeoutException": "Always" + "exceptions": { + "System.NotImplementedException": "Informational", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational" }, "disableXmlDocInterop": false, "disableLinqSupport": false, diff --git a/Test/Test.csproj b/Test/Test.csproj index e35409e..49abd8c 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -1,4 +1,4 @@ - + Exe @@ -17,19 +17,12 @@ --> - - all - runtime; build; native; contentfiles; analyzers - - - - + @@ -40,4 +33,5 @@ - \ No newline at end of file + + diff --git a/default-settings.json b/default-settings.json new file mode 100644 index 0000000..09be27b --- /dev/null +++ b/default-settings.json @@ -0,0 +1,18 @@ +{ + "exceptions": { + "System.ArgumentNullException": "Ignored", + "System.ArgumentOutOfRangeException": "Ignored", + "System.InvalidOperationException": "Informational", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational", + "System.InvalidCastException": "Informational", + "System.NullReferenceException": "Informational", + "System.OutOfMemoryException": "Informational", + "System.UnauthorizedAccessException": "Informational", + "System.Exception": "Strict", + "System.ApplicationException": "Strict", + "System.IO.EndOfStreamException": "Strict", + "System.Net.WebException": "Strict" + } +} + diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 7f56976..45ba990 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -485,17 +485,17 @@ You can customize how exceptions are reported by adding a `CheckedExceptions.set #### Example Configuration +A baseline template is available in `default-settings.json`. + Create a `CheckedExceptions.settings.json` file with the following structure: ```json { - "ignoredExceptions": [ - "System.ArgumentNullException" - ], - "informationalExceptions": { - "System.NotImplementedException": "Throw", - "System.IO.IOException": "Propagation", - "System.TimeoutException": "Always" + "exceptions": { + "System.ArgumentNullException": "Ignored", + "System.NotImplementedException": "Informational", + "System.IO.IOException": "Informational", + "System.TimeoutException": "Informational" } } ``` @@ -515,22 +515,9 @@ Add the settings file to your `.csproj`: ### Behavior -- **`ignoredExceptions`**: Exceptions listed here will be completely ignored—no diagnostics or error reports will be generated. -- **`informationalExceptions`**: Exceptions listed here will generate informational diagnostics but won't be reported as errors. - -### Informational Exceptions Modes - -The `informationalExceptions` section allows you to specify the context in which an exception should be treated as informational. The available modes are: - -| Mode | Description | -|---------------|-------------------------------------------------------------------------------------------------------------------| -| `Throw` | The exception is considered informational when thrown directly within the method. | -| `Propagation` | The exception is considered informational when propagated (re-thrown or passed up the call stack). | -| `Always` | The exception is always considered informational, regardless of context. | - -**Example Scenario:** - -- **`System.IO.IOException`**: When thrown directly (e.g., within a method), it might be critical. However, when propagated from a utility method like `System.Console.WriteLine`, it’s unlikely and can be treated as informational. +- **`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. +- **`Strict`**: Exceptions must be handled or declared; missing `[Throws]` results in warnings. ## Performance Considerations diff --git a/schemas/settings-schema.json b/schemas/settings-schema.json index 2483dc6..810d685 100644 --- a/schemas/settings-schema.json +++ b/schemas/settings-schema.json @@ -47,30 +47,19 @@ "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": { - "type": "string", - "pattern": "^!?[a-zA-Z0-9_.]+\\*?$" - }, - "description": "A list of fully qualified exception type names or glob-like patterns to be ignored. Entries may optionally start with '!' to indicate exceptions that should not be ignored." - }, - "informationalExceptions": { + "exceptions": { "type": "object", "additionalProperties": { "type": "string", "enum": [ - "Throw", - "Propagation", - "Always" + "Ignored", + "Informational", + "Strict" ] }, - "description": "A mapping of fully qualified exception type names to their informational handling strategy." + "description": "Explicit classification for exception types." } }, - "required": [ - "ignoredExceptions", - "informationalExceptions" - ], + "required": [], "additionalProperties": false } \ No newline at end of file From 7b1311e60b521f01d8541fe7573a00daae6ad09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 14:21:03 +0200 Subject: [PATCH 2/4] Expand baseline exception list --- CHANGELOG.md | 2 +- default-settings.json | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 129cfb6..ab51600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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) -- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide baseline exception classifications in `default-settings.json` +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Provide comprehensive baseline exception classifications in `default-settings.json` ### Changed diff --git a/default-settings.json b/default-settings.json index 09be27b..0b9afc0 100644 --- a/default-settings.json +++ b/default-settings.json @@ -2,17 +2,42 @@ "exceptions": { "System.ArgumentNullException": "Ignored", "System.ArgumentOutOfRangeException": "Ignored", + "System.ArgumentException": "Ignored", + "System.FormatException": "Ignored", + "System.InvalidOperationException": "Informational", - "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational", + "System.NotSupportedException": "Informational", "System.InvalidCastException": "Informational", "System.NullReferenceException": "Informational", + "System.IndexOutOfRangeException": "Informational", + "System.DivideByZeroException": "Informational", + "System.OverflowException": "Informational", + "System.ArithmeticException": "Informational", + "System.ObjectDisposedException": "Informational", "System.OutOfMemoryException": "Informational", + "System.StackOverflowException": "Informational", + "System.TypeInitializationException": "Informational", + "System.MemberAccessException": "Informational", "System.UnauthorizedAccessException": "Informational", + "System.ThreadAbortException": "Informational", + "System.ExecutionEngineException": "Informational", + "System.AccessViolationException": "Informational", + "System.AppDomainUnloadedException": "Informational", + "System.InsufficientMemoryException": "Informational", + "System.Runtime.InteropServices.SEHException": "Informational", + "System.Exception": "Strict", "System.ApplicationException": "Strict", + "System.IO.IOException": "Strict", "System.IO.EndOfStreamException": "Strict", - "System.Net.WebException": "Strict" + "System.IO.FileNotFoundException": "Strict", + "System.IO.DirectoryNotFoundException": "Strict", + "System.IO.PathTooLongException": "Strict", + "System.IO.DriveNotFoundException": "Strict", + "System.Net.WebException": "Strict", + "System.Net.Sockets.SocketException": "Strict", + "System.TimeoutException": "Strict", + "System.Data.SqlClient.SqlException": "Strict" } } From aac8fa4b33fc2b32e1142c2ef42184644ced22b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 14:35:28 +0200 Subject: [PATCH 3/4] Document explicit exception taxonomy and strict defaults --- CHANGELOG.md | 1 + README.md | 2 ++ docs/analyzer-specification.md | 10 ++++++---- docs/exception-handling.md | 11 ++++++++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab51600..198de36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 ## [2.2.3] - 2025-08-24 diff --git a/README.md b/README.md index 248a264..01d034c 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,8 @@ 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]`. + Add `CheckedExceptions.settings.json`: ```json diff --git a/docs/analyzer-specification.md b/docs/analyzer-specification.md index 49ffe94..6d52192 100644 --- a/docs/analyzer-specification.md +++ b/docs/analyzer-specification.md @@ -335,13 +335,15 @@ public int Value { set => throw new InvalidOperationException(); } --- -## Ignored exceptions (Core analysis) +## Exception classification (Core analysis) -You can configure ignored exception types in `CheckedExceptions.settings.json`. +`CheckedExceptions.settings.json` contains an explicit `exceptions` map that classifies each exception type as `Ignored`, `Informational`, or `Strict`. -Ignored exceptions will not produce *unhandled* diagnostics, but are still reported for awareness: +- **Ignored** – no diagnostics are produced. +- **Informational** – diagnostics are reported but `[Throws]` is not required. +- **Strict** – exceptions must be caught or declared. -* **Ignored exception propagated** → **`THROW002`** +Any exception not listed defaults to **Strict**, so unclassified types will trigger `THROW001` until they are handled or declared. --- diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 45ba990..1fc2c0a 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -43,10 +43,13 @@ This document outlines the behavior of the analyzer. ## Overview -The **CheckedExceptions Analyzer** enhances exception management in your C# projects by: +The **CheckedExceptions Analyzer** serves three purposes: -1. **Identifying Exception Sources**: Detecting `throw` statements or method calls where exceptions may be thrown or propagated. -2. **Reporting Diagnostics**: Flagging unhandled exceptions, prompting developers to handle them explicitly or declare their propagation. +1. **Discover potential exceptions** so you know which code paths might fail. +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]`. --- @@ -519,6 +522,8 @@ Add the settings file to your `.csproj`: - **`Informational`**: Exceptions generate informational diagnostics but do not require `[Throws]` declarations. - **`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**. + ## Performance Considerations The analyzer operates during the compilation process and is designed to have minimal impact on build performance. By leveraging existing compiler mechanisms and efficient code analysis techniques, it ensures that your development workflow remains smooth. From f0084dc324de7e55f59471dff22ae84850ec8695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20Sundstr=C3=B6m?= Date: Fri, 5 Sep 2025 14:44:46 +0200 Subject: [PATCH 4/4] Translate legacy configuration lists --- CHANGELOG.md | 4 ++ .../CSharpAnalyzerVerifier.cs | 10 +++-- CheckedExceptions/AnalyzerSettings.cs | 38 +++++++++++++++++++ README.md | 2 + docs/analyzer-specification.md | 2 + docs/exception-handling.md | 2 + schemas/settings-schema.json | 12 ++++++ 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 198de36..674f5c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ 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) 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 +### Deprecated + +- PR [#PR_NUMBER](https://github.com/marinasundstrom/CheckedExceptions/pull/PR_NUMBER) Support legacy `ignoredExceptions` and `informationalExceptions` settings by translating them to `exceptions` + ## [2.2.3] - 2025-08-24 ### Fixed diff --git a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs index 9d081e9..6c706c6 100644 --- a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs +++ b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs @@ -92,10 +92,12 @@ await VerifyAnalyzerAsync(source, (test) => test.TestState.AdditionalFiles.Add(("CheckedExceptions.settings.json", """" { - "exceptions": { - "System.NotImplementedException": "Ignored", - "System.IO.IOException": "Informational", - "System.TimeoutException": "Informational" + "ignoredExceptions": [ + "System.NotImplementedException" + ], + "informationalExceptions": { + "System.IO.IOException": "Propagation", + "System.TimeoutException": "Propagation" } } """")); diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index 32f89af..4aac697 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -66,6 +66,44 @@ public partial class AnalyzerSettings [JsonPropertyName("exceptions")] public IDictionary Exceptions { get; set; } = new Dictionary(); + + [JsonPropertyName("ignoredExceptions")] + [Obsolete("Use 'exceptions' instead.")] + public IList? IgnoredExceptions + { + get => null; + set + { + if (value is null) + { + return; + } + + foreach (var exception in value) + { + Exceptions[exception] = ExceptionClassification.Ignored; + } + } + } + + [JsonPropertyName("informationalExceptions")] + [Obsolete("Use 'exceptions' instead.")] + public IDictionary? InformationalExceptions + { + get => null; + set + { + if (value is null) + { + return; + } + + foreach (var exception in value.Keys) + { + Exceptions[exception] = ExceptionClassification.Informational; + } + } + } } [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/README.md b/README.md index 01d034c..7958ed8 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,8 @@ Add `CheckedExceptions.settings.json`: } ``` +> **Migration note:** Legacy `ignoredExceptions` and `informationalExceptions` settings are still recognized but have been deprecated. They are automatically translated to the unified `exceptions` map. + > **Control flow analysis** powers redundancy checks (e.g. unreachable code, redundant catches, unused exception declarations). > Disabling it may improve analyzer performance slightly at the cost of precision. diff --git a/docs/analyzer-specification.md b/docs/analyzer-specification.md index 6d52192..385a939 100644 --- a/docs/analyzer-specification.md +++ b/docs/analyzer-specification.md @@ -345,6 +345,8 @@ public int Value { set => throw new InvalidOperationException(); } Any exception not listed defaults to **Strict**, so unclassified types will trigger `THROW001` 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. + --- ## Casts and conversions (Core analysis, refined by control flow) diff --git a/docs/exception-handling.md b/docs/exception-handling.md index 1fc2c0a..2d1b0d6 100644 --- a/docs/exception-handling.md +++ b/docs/exception-handling.md @@ -505,6 +505,8 @@ Create a `CheckedExceptions.settings.json` file with the following structure: There is a JSON schema provided. +> **Migration note:** The older `ignoredExceptions` and `informationalExceptions` settings are still understood but deprecated. Entries are automatically converted into the `exceptions` dictionary. + **Note:** Ignoring `System.ArgumentNullException` may not be necessary when nullable annotations are enabled, as the analyzer already handles this scenario. ### Registering the File diff --git a/schemas/settings-schema.json b/schemas/settings-schema.json index 810d685..5670418 100644 --- a/schemas/settings-schema.json +++ b/schemas/settings-schema.json @@ -47,6 +47,18 @@ "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": { "type": "string" }, + "deprecated": true, + "description": "Deprecated. Use 'exceptions' instead." + }, + "informationalExceptions": { + "type": "object", + "additionalProperties": { "type": "string" }, + "deprecated": true, + "description": "Deprecated. Use 'exceptions' instead." + }, "exceptions": { "type": "object", "additionalProperties": {