diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df8633..674f5c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,16 @@ 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 comprehensive 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 +- 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 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..6c706c6 100644 --- a/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs +++ b/CheckedExceptions.Tests/CSharpAnalyzerVerifier.cs @@ -96,8 +96,8 @@ await VerifyAnalyzerAsync(source, (test) => "System.NotImplementedException" ], "informationalExceptions": { - "System.IO.IOException": "Always", - "System.TimeoutException": "Always" + "System.IO.IOException": "Propagation", + "System.TimeoutException": "Propagation" } } """")); 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..4aac697 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -64,17 +64,52 @@ public partial class AnalyzerSettings [JsonIgnore] internal bool TreatThrowsExceptionAsCatchRestEnabled => TreatThrowsExceptionAsCatchRest; + [JsonPropertyName("exceptions")] + public IDictionary Exceptions { get; set; } = new Dictionary(); + [JsonPropertyName("ignoredExceptions")] - public IEnumerable IgnoredExceptions { get; set; } = new List(); + [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")] - public IDictionary InformationalExceptions { get; set; } = new Dictionary(); + [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))] -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..7958ed8 100644 --- a/README.md +++ b/README.md @@ -128,21 +128,19 @@ dotnet_diagnostic.THROW003.severity = warning ### JSON Settings +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 { - // 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). @@ -176,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/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..0b9afc0 --- /dev/null +++ b/default-settings.json @@ -0,0 +1,43 @@ +{ + "exceptions": { + "System.ArgumentNullException": "Ignored", + "System.ArgumentOutOfRangeException": "Ignored", + "System.ArgumentException": "Ignored", + "System.FormatException": "Ignored", + + "System.InvalidOperationException": "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.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" + } +} + diff --git a/docs/analyzer-specification.md b/docs/analyzer-specification.md index 49ffe94..385a939 100644 --- a/docs/analyzer-specification.md +++ b/docs/analyzer-specification.md @@ -335,13 +335,17 @@ 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. + +> **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 7f56976..2d1b0d6 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]`. --- @@ -485,23 +488,25 @@ 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" } } ``` 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 @@ -515,22 +520,11 @@ 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:** +- **`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. -- **`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. +Any exception type that doesn't appear in the `exceptions` map defaults to **Strict**. ## Performance Considerations diff --git a/schemas/settings-schema.json b/schemas/settings-schema.json index 2483dc6..5670418 100644 --- a/schemas/settings-schema.json +++ b/schemas/settings-schema.json @@ -49,28 +49,29 @@ }, "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." + "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": { "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