Skip to content

Commit 078f877

Browse files
Mark redundant throws in LINQ lambdas (#293)
1 parent f8abf38 commit 078f877

File tree

6 files changed

+89
-43
lines changed

6 files changed

+89
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
- PR [#287](https://github.com/marinasundstrom/CheckedExceptions/pull/287) Fix LINQ chain diagnostics
2424
- PR [#294](https://github.com/marinasundstrom/CheckedExceptions/pull/294) Enable batch fixing for catch-clause, try-catch, and redundant catch clause code fixes
25+
- PR [#293](https://github.com/marinasundstrom/CheckedExceptions/pull/293) Mark throws declarations in LINQ lambdas as redundant when implicitly declared exceptions are enabled
2526

2627
## [2.1.2] - 2025-08-22
2728

CheckedExceptions.Tests/CheckedExceptionsAnalyzerTests.Linq.cs

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,25 @@ public async Task QueryOperator()
2525
var expected = Verifier.UnhandledException("FormatException")
2626
.WithSpan(8, 15, 8, 22);
2727

28-
var expected2 = Verifier.UnhandledException("OverflowException")
29-
.WithSpan(8, 15, 8, 22);
30-
31-
var expected3 = Verifier.UnhandledException("InvalidOperationException")
32-
.WithSpan(8, 15, 8, 22);
33-
34-
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
35-
{
36-
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3);
37-
}, executable: true);
28+
var expected2 = Verifier.UnhandledException("OverflowException")
29+
.WithSpan(8, 15, 8, 22);
30+
31+
var expected3 = Verifier.UnhandledException("InvalidOperationException")
32+
.WithSpan(8, 15, 8, 22);
33+
34+
var expected4 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
35+
.WithArguments("FormatException")
36+
.WithSpan(7, 40, 7, 55);
37+
38+
var expected5 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
39+
.WithArguments("OverflowException")
40+
.WithSpan(7, 65, 7, 82);
41+
42+
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
43+
{
44+
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3, expected4, expected5);
45+
o.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration);
46+
}, executable: true);
3847
}
3948

4049
[Fact]
@@ -61,10 +70,19 @@ public async Task AsyncQueryOperator()
6170
var expected3 = Verifier.UnhandledException("InvalidOperationException")
6271
.WithSpan(9, 21, 9, 33);
6372

73+
var expected4 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
74+
.WithArguments("FormatException")
75+
.WithSpan(8, 40, 8, 55);
76+
77+
var expected5 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
78+
.WithArguments("OverflowException")
79+
.WithSpan(8, 65, 8, 82);
80+
6481
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
6582
{
6683
o.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(System.Linq.AsyncEnumerable).Assembly.Location));
67-
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3);
84+
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3, expected4, expected5);
85+
o.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration);
6886
}, executable: true);
6987
}
7088

@@ -122,17 +140,25 @@ public async Task ForEach()
122140
}
123141
""";
124142

125-
var expected = Verifier.UnhandledException("FormatException")
126-
.WithSpan(8, 22, 8, 27);
127-
128-
var expected2 = Verifier.UnhandledException("OverflowException")
129-
.WithSpan(8, 22, 8, 27);
130-
131-
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
132-
{
133-
o.ExpectedDiagnostics.AddRange(expected, expected2);
134-
}, executable: true);
135-
}
143+
var expected = Verifier.UnhandledException("FormatException")
144+
.WithSpan(8, 22, 8, 27);
145+
146+
var expected2 = Verifier.UnhandledException("OverflowException")
147+
.WithSpan(8, 22, 8, 27);
148+
var expected3 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
149+
.WithArguments("FormatException")
150+
.WithSpan(7, 40, 7, 55);
151+
152+
var expected4 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
153+
.WithArguments("OverflowException")
154+
.WithSpan(7, 65, 7, 82);
155+
156+
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
157+
{
158+
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3, expected4);
159+
o.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration);
160+
}, executable: true);
161+
}
136162

137163
[Fact]
138164
public async Task PassDelegateByVariable()
@@ -149,12 +175,11 @@ public async Task PassDelegateByVariable()
149175
foreach (var item in query) { }
150176
""";
151177

152-
var expected = Verifier.UnhandledException("FormatException")
153-
.WithSpan(9, 22, 9, 27);
154-
155-
var expected2 = Verifier.UnhandledException("OverflowException")
156-
.WithSpan(9, 22, 9, 27);
157-
178+
var expected = Verifier.UnhandledException("FormatException")
179+
.WithSpan(9, 22, 9, 27);
180+
181+
var expected2 = Verifier.UnhandledException("OverflowException")
182+
.WithSpan(9, 22, 9, 27);
158183
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
159184
{
160185
o.ExpectedDiagnostics.AddRange(expected, expected2);
@@ -181,7 +206,6 @@ public async Task PassDelegateByVariable_Chained()
181206

182207
var expected2 = Verifier.UnhandledException("OverflowException")
183208
.WithSpan(9, 22, 9, 27);
184-
185209
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
186210
{
187211
o.ExpectedDiagnostics.AddRange(expected, expected2);
@@ -205,18 +229,26 @@ public async Task CastOperator()
205229
foreach (var item in query) { }
206230
""";
207231

208-
var expected = Verifier.UnhandledException("FormatException")
209-
.WithSpan(11, 22, 11, 27);
210-
211-
var expected2 = Verifier.UnhandledException("OverflowException")
212-
.WithSpan(11, 22, 11, 27);
213-
214-
var expected3 = Verifier.UnhandledException("InvalidCastException")
215-
.WithSpan(11, 22, 11, 27);
216-
232+
var expected = Verifier.UnhandledException("FormatException")
233+
.WithSpan(11, 22, 11, 27);
234+
235+
var expected2 = Verifier.UnhandledException("OverflowException")
236+
.WithSpan(11, 22, 11, 27);
237+
238+
var expected3 = Verifier.UnhandledException("InvalidCastException")
239+
.WithSpan(11, 22, 11, 27);
240+
var expected4 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
241+
.WithArguments("FormatException")
242+
.WithSpan(8, 27, 8, 42);
243+
244+
var expected5 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration)
245+
.WithArguments("OverflowException")
246+
.WithSpan(8, 52, 8, 69);
247+
217248
await Verifier.VerifyAnalyzerAsync(test, setup: o =>
218249
{
219-
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3);
250+
o.ExpectedDiagnostics.AddRange(expected, expected2, expected3, expected4, expected5);
251+
o.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantExceptionDeclaration);
220252
}, executable: true);
221253
}
222254

CheckedExceptions/CheckedExceptionsAnalyzer.ControlFlow.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private static void AnalyzeControlFlow(
8484
{
8585
var semanticModel = context.SemanticModel;
8686
var node = context.Node;
87+
var settings = GetAnalyzerSettings(context.Options);
8788

8889
IMethodSymbol? methodSymbol = null;
8990

@@ -103,6 +104,18 @@ private static void AnalyzeControlFlow(
103104
var declared = throwsAttributes.SelectMany(x => GetExceptionTypes(x, semanticModel))
104105
.ToImmutableHashSet(SymbolEqualityComparer.Default);
105106

107+
if (settings.IsLinqImplicitlyDeclaredExceptionsEnabled &&
108+
IsInsideLinqLambda(node, semanticModel, settings, out _))
109+
{
110+
foreach (var declaredType in declared)
111+
{
112+
var location = GetThrowsAttributeLocation(methodSymbol, (INamedTypeSymbol?)declaredType!, context.Compilation)
113+
?? node.GetLocation();
114+
ReportRedundantExceptionDeclaration(context.ReportDiagnostic, declaredType, location);
115+
}
116+
return;
117+
}
118+
106119
// Collect all actually escaping exceptions
107120
var actual = CollectThrownExceptions(methodSymbol, semanticModel.Compilation, semanticModel, context.ReportDiagnostic, context.Options);
108121

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Add `CheckedExceptions.settings.json`:
151151
// If true, analysis on LINQ constructs will be disabled (default: false).
152152
"disableLinqSupport": false,
153153

154-
// If true, exceptions in LINQ lambdas do not have to be declared (default: false).
154+
// If true, implicit exception inference in LINQ lambdas is disabled and declarations are required (default: false).
155155
"disableLinqImplicitlyDeclaredExceptions": false,
156156

157157
// If true, no diagnostics will be issued on contract boundaries, such as arguments to methods and return statements (default: false).
@@ -317,7 +317,7 @@ var tens = values
317317
.ToArray(); // THROW001: unhandled FormatException, OverflowException
318318
```
319319

320-
> Exceptions are inferred and implicit on LINQ methods, so no declarations needed. this behavior can be disabled.
320+
> Exceptions are inferred and implicit on LINQ methods. Any explicit `[Throws]` on LINQ lambdas is flagged as redundant. This behavior can be disabled.
321321
322322
Read about it [here](docs/linq-support.md).
323323

docs/analyzer-specification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ This option disables analysis of LINQ operators defined on `Queryable`. Expressi
503503

504504
#### Disable implicitly declared exceptions in lambdas
505505

506-
This option control whether to disable implicitly declared exceptions in lambdas passed into LINQ operator methods.
506+
This option controls whether to disable implicitly declared exceptions in lambdas passed into LINQ operator methods. When enabled (the default), exceptions are inferred and any `[Throws]` declarations on these lambdas are reported as redundant.
507507

508508
```json
509509
{

docs/linq-support.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var allEven = values
2626
// THROW001: Unhandled exception type 'OverflowException'
2727
```
2828

29-
> Exceptions are inferred and implicit on LINQ methods, so no declarations needed. this behavior can be disabled.
29+
> Exceptions are inferred and implicit on LINQ methods. Any explicit `[Throws]` on LINQ lambdas is reported as redundant. This behavior can be disabled.
3030
3131
This differs from `First()`/`Single()` cases by not adding its own “empty/duplicate” errors—`All` only reflects exceptions from the predicate.
3232

0 commit comments

Comments
 (0)