diff --git a/CHANGELOG.md b/CHANGELOG.md index 749b7d5..62c4a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- PR [#306](https://github.com/marinasundstrom/CheckedExceptions/pull/306) Account for LINQ pipeline exceptions when analyzing foreach statements inside try-catch blocks + ## [2.5.0] - 2025-09-05 ### Added diff --git a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.Linq.cs b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.Linq.cs index 812ad6e..3a38d86 100644 --- a/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.Linq.cs +++ b/CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.Linq.cs @@ -158,11 +158,45 @@ await Verifier.VerifyAnalyzerAsync(test, setup: o => o.ExpectedDiagnostics.AddRange(expected, expected2, expected3, expected4); o.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration); }, executable: true); - } - - [Fact] - public async Task PassDelegateByVariable() - { + } + + [Fact] + public async Task ForEachCaughtByCatchAll() + { + var test = /* lang=c#-test */ """ + #nullable enable + using System; + using System.Collections.Generic; + using System.Linq; + + IEnumerable items = []; + var query = items.Where(x => int.Parse("10") == x); + try + { + foreach (var item in query) { } + } + catch + { + } + """; + + var expected = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdImplicitlyDeclaredException) + .WithArguments("FormatException") + .WithSpan(7, 34, 7, 45); + + var expected2 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdImplicitlyDeclaredException) + .WithArguments("OverflowException") + .WithSpan(7, 34, 7, 45); + + await Verifier.VerifyAnalyzerAsync(test, setup: o => + { + o.ExpectedDiagnostics.AddRange(expected, expected2); + }, executable: true); + } + + [Fact] + public async Task PassDelegateByVariable() + { var test = /* lang=c#-test */ """ #nullable enable using System; diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs index 398f886..9ddf9a2 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Analysis.cs @@ -376,6 +376,13 @@ private static void CollectExceptionsFromExpression(SyntaxNode expression, Compi } } } + + // Capture exceptions from deferred enumerable pipelines directly referenced by the expression itself + var exprOp = semanticModel.GetOperation(expression); + if (exprOp is not null) + { + CollectEnumerationExceptions(exprOp, exceptions, compilation, semanticModel, settings, default); + } } private static HashSet? GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) diff --git a/Test/CheckedExceptions.settings.json b/Test/CheckedExceptions.settings.json index b7c9add..daa4b06 100644 --- a/Test/CheckedExceptions.settings.json +++ b/Test/CheckedExceptions.settings.json @@ -1,5 +1,5 @@ { - "defaultExceptionClassification": "NonStrict", + "defaultExceptionClassification": "Strict", "exceptions": { "System.NotImplementedException": "Informational", "System.IO.IOException": "Informational", diff --git a/Test/LinqTest.cs b/Test/LinqTest.cs index b9e9520..0cab116 100644 --- a/Test/LinqTest.cs +++ b/Test/LinqTest.cs @@ -22,16 +22,41 @@ public void Test1() private static void ExplicitlyDeclaredThrows() { IEnumerable items = []; - var query = items.Where([Throws(typeof(FormatException), typeof(OverflowException))] (x) => x == int.Parse("10")); + var query = items.Where((x) => x == int.Parse("10")); var x = query; var r = x.Select((z) => 2); foreach (var n in query) { } + try + { + foreach (var n in query) { } + } + catch (OverflowException exc) + { + + } + catch + { + + } + foreach (var item in r) { } + + try + { + foreach (var item in r) + { + + } + } + catch + { + + } } private static void ImplicitlyDeclaredThrows()