Skip to content

Commit 82ece29

Browse files
Enable batch fixing for try/catch code fixes #275 (#294)
* Enable batch fixing for redundant catch clause remover * Add PR number to CHANGELOG.md
1 parent 4853835 commit 82ece29

7 files changed

+272
-42
lines changed

CHANGELOG.md

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

2222
- PR [#287](https://github.com/marinasundstrom/CheckedExceptions/pull/287) Fix LINQ chain diagnostics
23+
- PR [#294](https://github.com/marinasundstrom/CheckedExceptions/pull/294) Enable batch fixing for catch-clause, try-catch, and redundant catch clause code fixes
2324

2425
## [2.1.2] - 2025-08-22
2526

CheckedExceptions.CodeFixes/AddCatchClauseToTryCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class AddCatchClauseToTryCodeFixProvider : CodeFixProvider
2020
[CheckedExceptionsAnalyzer.DiagnosticIdUnhandled];
2121

2222
public sealed override FixAllProvider GetFixAllProvider() =>
23-
null!;
23+
WellKnownFixAllProviders.BatchFixer;
2424

2525
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2626
{

CheckedExceptions.CodeFixes/RemoveRedundantCatchClauseCodeFixProvider.cs

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -33,83 +33,69 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3333
var node = root.FindNode(diagnostic.Location.SourceSpan);
3434

3535
var catchClause = node.AncestorsAndSelf().OfType<CatchClauseSyntax>().First();
36-
3736
var tryStatement = catchClause.Parent as TryStatementSyntax;
3837

3938
string title = TitleRemoveRedundantCatchClause;
4039

41-
if (tryStatement is not null)
40+
if (diagnostics.Length is 1 && tryStatement?.Catches.Count is 1)
4241
{
43-
if (tryStatement.Catches.Count is 1)
44-
{
45-
title = title.Replace(title, "Remove redundant try/catch");
46-
}
42+
title = "Remove redundant try/catch";
4743
}
4844

4945
context.RegisterCodeFix(
5046
CodeAction.Create(
5147
title: title,
52-
createChangedDocument: c => RemoveRedundantCatchClauseAsync(context.Document, catchClause, diagnostics, c),
48+
createChangedDocument: c => RemoveRedundantCatchClausesAsync(context.Document, diagnostics, c),
5349
equivalenceKey: TitleRemoveRedundantCatchClause),
5450
diagnostics);
5551
}
5652

57-
private async Task<Document> RemoveRedundantCatchClauseAsync(Document document, CatchClauseSyntax catchClause, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
53+
private async Task<Document> RemoveRedundantCatchClausesAsync(Document document, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
5854
{
59-
var exceptionTypeNames = diagnostics
60-
.Select(d => d.Properties.TryGetValue("ExceptionType", out var type) ? type! : string.Empty);
61-
6255
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
63-
if (root is null) return document;
56+
if (root is null)
57+
{
58+
return document;
59+
}
6460

65-
var tryStatement = catchClause.Parent as TryStatementSyntax;
61+
var orderedDiagnostics = diagnostics
62+
.OrderByDescending(d => d.Location.SourceSpan.Start);
63+
64+
var newRoot = root;
6665

67-
if (tryStatement is not null)
66+
foreach (var diagnostic in orderedDiagnostics)
6867
{
69-
if (tryStatement.Catches.Count is 1)
70-
{
71-
// Get the current node in the tree
72-
var nodeInRoot = root.FindNode(tryStatement.Span);
68+
var clause = newRoot.FindNode(diagnostic.Location.SourceSpan).AncestorsAndSelf().OfType<CatchClauseSyntax>().First();
69+
var tryStatement = clause.Parent as TryStatementSyntax;
7370

74-
// What we want to insert instead
71+
if (tryStatement is not null && tryStatement.Catches.Count == 1)
72+
{
73+
var nodeInRoot = newRoot.FindNode(tryStatement.Span);
7574
var liftedStatements = tryStatement.Block.Statements;
7675

77-
SyntaxNode newRoot;
78-
7976
if (nodeInRoot is GlobalStatementSyntax global)
8077
{
81-
// Wrap each lifted statement in its own GlobalStatementSyntax
82-
var newGlobals = liftedStatements.Select(
83-
s => GlobalStatement(s)
84-
.WithLeadingTrivia(global.GetLeadingTrivia())
85-
.WithTrailingTrivia(global.GetTrailingTrivia()));
78+
var newGlobals = liftedStatements.Select(s => GlobalStatement(s)
79+
.WithLeadingTrivia(global.GetLeadingTrivia())
80+
.WithTrailingTrivia(global.GetTrailingTrivia()));
8681

87-
newRoot = root.ReplaceNode(global, newGlobals);
82+
newRoot = newRoot.ReplaceNode(global, newGlobals);
8883
}
8984
else if (nodeInRoot is TryStatementSyntax tryNode)
9085
{
91-
// Normal case inside a block
92-
9386
var annotatedStatements = liftedStatements
9487
.Select(s => s.WithAdditionalAnnotations(Formatter.Annotation));
9588

96-
newRoot = root.ReplaceNode(tryNode, annotatedStatements);
89+
newRoot = newRoot.ReplaceNode(tryNode, annotatedStatements);
9790
}
98-
else
99-
{
100-
// Fallback (shouldn’t really happen)
101-
return document;
102-
}
103-
104-
return document.WithSyntaxRoot(newRoot);
10591
}
10692
else
10793
{
108-
var newRoot = root.RemoveNode(catchClause, SyntaxRemoveOptions.AddElasticMarker);
109-
return document.WithSyntaxRoot(newRoot);
94+
var currentClause = newRoot.FindNode(clause.Span).AncestorsAndSelf().OfType<CatchClauseSyntax>().First();
95+
newRoot = newRoot.RemoveNode(currentClause, SyntaxRemoveOptions.AddElasticMarker);
11096
}
11197
}
11298

113-
return document;
99+
return document.WithSyntaxRoot(newRoot);
114100
}
115101
}

CheckedExceptions.CodeFixes/SurroundWithTryCatchCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class SurroundWithTryCatchCodeFixProvider : CodeFixProvider
2323
CheckedExceptionsAnalyzer.DiagnosticIdImplicitlyDeclaredException];
2424

2525
public sealed override FixAllProvider GetFixAllProvider() =>
26-
null!;
26+
WellKnownFixAllProviders.BatchFixer;
2727

2828
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2929
{

CheckedExceptions.Tests/CodeFixes/AddCatchClauseToTryCodeFixProviderTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace Sundstrom.CheckedExceptions.Tests.CodeFixes;
22

33
using System.Threading.Tasks;
44

5+
using Microsoft.CodeAnalysis.Testing;
56
using Xunit;
67
using Xunit.Abstractions;
78

@@ -105,4 +106,90 @@ public void TestMethod()
105106

106107
await Verifier.VerifyCodeFixAsync(testCode, expectedDiagnostic, fixedCode, expectedIncrementalIterations: 0);
107108
}
109+
110+
[Fact]
111+
public async Task FixAll_AddsCatchClauses_ForMultipleUnhandledExceptions()
112+
{
113+
var testCode = /* lang=c#-test */ """
114+
using System;
115+
116+
namespace TestNamespace
117+
{
118+
public class TestClass
119+
{
120+
public void TestMethod1()
121+
{
122+
try
123+
{
124+
throw new InvalidOperationException();
125+
}
126+
catch (ArgumentException ex)
127+
{
128+
}
129+
}
130+
131+
public void TestMethod2()
132+
{
133+
try
134+
{
135+
throw new ArgumentException();
136+
}
137+
catch (InvalidOperationException ex)
138+
{
139+
}
140+
}
141+
}
142+
}
143+
""";
144+
145+
var fixedCode = /* lang=c#-test */ """
146+
using System;
147+
148+
namespace TestNamespace
149+
{
150+
public class TestClass
151+
{
152+
public void TestMethod1()
153+
{
154+
try
155+
{
156+
throw new InvalidOperationException();
157+
}
158+
catch (ArgumentException ex)
159+
{
160+
}
161+
catch (InvalidOperationException invalidOperationException)
162+
{
163+
}
164+
}
165+
166+
public void TestMethod2()
167+
{
168+
try
169+
{
170+
throw new ArgumentException();
171+
}
172+
catch (InvalidOperationException ex)
173+
{
174+
}
175+
catch (ArgumentException argumentException)
176+
{
177+
}
178+
}
179+
}
180+
}
181+
""";
182+
183+
var expectedDiagnostic1 = Verifier.UnhandledException("InvalidOperationException")
184+
.WithSpan(11, 17, 11, 55);
185+
var expectedDiagnostic2 = Verifier.UnhandledException("ArgumentException")
186+
.WithSpan(22, 17, 22, 47);
187+
188+
await Verifier.VerifyCodeFixAsync(
189+
testCode,
190+
[expectedDiagnostic1, expectedDiagnostic2],
191+
fixedCode,
192+
expectedIncrementalIterations: 2,
193+
setup: test => test.CodeFixTestBehaviors &= ~CodeFixTestBehaviors.SkipFixAllCheck);
194+
}
108195
}

CheckedExceptions.Tests/CodeFixes/RemoveRedundantCatchClauseCodeFixProviderTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace Sundstrom.CheckedExceptions.Tests.CodeFixes;
22

33
using System.Threading.Tasks;
44

5+
using Microsoft.CodeAnalysis.Testing;
56
using Xunit;
67
using Xunit.Abstractions;
78

@@ -187,4 +188,91 @@ await Verifier.VerifyCodeFixAsync(testCode, [expectedDiagnostic], fixedCode, exe
187188
option.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause);
188189
});
189190
}
191+
192+
[Fact]
193+
public async Task FixAll_RemovesRedundantCatchClausesAcrossMethods()
194+
{
195+
var testCode = /* lang=c#-test */ """
196+
#nullable enable
197+
using System;
198+
199+
class TestClass
200+
{
201+
void Method1()
202+
{
203+
try
204+
{
205+
int.Parse("a");
206+
}
207+
catch (FormatException ex)
208+
{
209+
}
210+
catch (OverflowException ex)
211+
{
212+
}
213+
catch (ArgumentException ex)
214+
{
215+
}
216+
}
217+
218+
void Method2()
219+
{
220+
try
221+
{
222+
int x = 0;
223+
string y = "";
224+
}
225+
catch (FormatException ex)
226+
{
227+
}
228+
}
229+
}
230+
""";
231+
232+
var fixedCode = /* lang=c#-test */ """
233+
#nullable enable
234+
using System;
235+
236+
class TestClass
237+
{
238+
void Method1()
239+
{
240+
try
241+
{
242+
int.Parse("a");
243+
}
244+
catch (FormatException ex)
245+
{
246+
}
247+
catch (OverflowException ex)
248+
{
249+
}
250+
}
251+
252+
void Method2()
253+
{
254+
int x = 0;
255+
string y = "";
256+
}
257+
}
258+
""";
259+
260+
var expectedDiagnostic1 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause)
261+
.WithArguments("ArgumentException")
262+
.WithSpan(18, 16, 18, 33);
263+
var expectedDiagnostic2 = Verifier.Diagnostic(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause)
264+
.WithArguments("FormatException")
265+
.WithSpan(30, 16, 30, 31);
266+
267+
await Verifier.VerifyCodeFixAsync(
268+
testCode,
269+
[expectedDiagnostic1, expectedDiagnostic2],
270+
fixedCode,
271+
expectedIncrementalIterations: 2,
272+
setup: test =>
273+
{
274+
test.CodeFixTestBehaviors &= ~CodeFixTestBehaviors.SkipFixAllCheck;
275+
test.DisabledDiagnostics.Remove(CheckedExceptionsAnalyzer.DiagnosticIdRedundantTypedCatchClause);
276+
});
277+
}
190278
}

0 commit comments

Comments
 (0)