Skip to content

Commit c7f1300

Browse files
joshsmithxrmclaude
andcommitted
refactor(tui): migrate SqlQueryScreen to TuiScreenBase, remove all PPDS013 pragmas
I2: SqlQueryScreen now extends TuiScreenBase — removes manual Content view, hotkey registration/cleanup, dispose boilerplate, and _environmentUrl/_session/_errorService fields. Uses Session, ErrorService, ScreenCancellation, RegisterHotkey(), RequestClose(), NotifyMenuChanged() from base class. CancellationToken.None replaced with ScreenCancellation throughout. I4: Remove all remaining #pragma PPDS013 blocks from dialogs. Update PPDS013 analyzer to recognize FireAndForget (including null-conditional ?.FireAndForget) as a safe pattern, eliminating the need for pragma suppression at call sites. Migrate ContinueWith error handling in ClearAllProfilesDialog, EnvironmentDetailsDialog, ExportDialog, ProfileCreationDialog, ProfileSelectorDialog, QueryHistoryDialog to use FireAndForget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8149946 commit c7f1300

File tree

10 files changed

+143
-328
lines changed

10 files changed

+143
-328
lines changed

src/PPDS.Analyzers/Rules/NoFireAndForgetInCtorAnalyzer.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ private static void AnalyzeConstructor(SyntaxNodeAnalysisContext context)
6767
if (HasContinueWithErrorHandling(invocation))
6868
continue;
6969

70+
// Skip if passed as argument to FireAndForget (centralized error handler)
71+
if (IsArgumentToFireAndForget(invocation))
72+
continue;
73+
7074
var methodName = GetMethodName(invocation);
7175

7276
var diagnostic = Diagnostic.Create(
@@ -168,6 +172,47 @@ private static bool HasContinueWithErrorHandling(InvocationExpressionSyntax invo
168172
return false;
169173
}
170174

175+
private static bool IsArgumentToFireAndForget(InvocationExpressionSyntax invocation)
176+
{
177+
// Check if this invocation is an argument to a FireAndForget call
178+
// Pattern 1: _errorService.FireAndForget(SomeAsync(), "context")
179+
// Pattern 2: _errorService?.FireAndForget(SomeAsync(), "context")
180+
if (invocation.Parent is ArgumentSyntax argument &&
181+
argument.Parent is ArgumentListSyntax argumentList)
182+
{
183+
// Direct call: errorService.FireAndForget(task, ...)
184+
if (argumentList.Parent is InvocationExpressionSyntax outerInvocation)
185+
{
186+
var outerName = GetInvocationName(outerInvocation);
187+
if (outerName == "FireAndForget")
188+
return true;
189+
}
190+
191+
// Null-conditional call: errorService?.FireAndForget(task, ...)
192+
// Syntax tree: ConditionalAccessExpression > InvocationExpression > ArgumentList
193+
if (argumentList.Parent is InvocationExpressionSyntax conditionalInvocation &&
194+
conditionalInvocation.Parent is ConditionalAccessExpressionSyntax)
195+
{
196+
var name = GetInvocationName(conditionalInvocation);
197+
if (name == "FireAndForget")
198+
return true;
199+
}
200+
}
201+
202+
return false;
203+
}
204+
205+
private static string? GetInvocationName(InvocationExpressionSyntax invocation)
206+
{
207+
return invocation.Expression switch
208+
{
209+
MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.Text,
210+
MemberBindingExpressionSyntax memberBinding => memberBinding.Name.Identifier.Text,
211+
IdentifierNameSyntax identifier => identifier.Identifier.Text,
212+
_ => null
213+
};
214+
}
215+
171216
private static string GetMethodName(InvocationExpressionSyntax invocation)
172217
{
173218
return invocation.Expression switch

src/PPDS.Cli/Tui/Dialogs/ClearAllProfilesDialog.cs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ namespace PPDS.Cli.Tui.Dialogs;
2121
internal sealed class ClearAllProfilesDialog : TuiDialog, ITuiStateCapture<ClearAllProfilesDialogState>
2222
{
2323
private readonly IProfileService _profileService;
24+
private readonly ITuiErrorService? _errorService;
2425
private readonly TextField _confirmationField;
2526
private readonly Button _clearButton;
2627
private readonly int _profileCount;
@@ -40,6 +41,7 @@ public ClearAllProfilesDialog(IProfileService profileService, int profileCount,
4041
: base("Clear All Profiles", session)
4142
{
4243
_profileService = profileService ?? throw new ArgumentNullException(nameof(profileService));
44+
_errorService = session?.GetErrorService();
4345
_profileCount = profileCount;
4446

4547
Width = 65;
@@ -171,20 +173,7 @@ private void OnClearClicked()
171173
}
172174

173175
// Perform the clear operation
174-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
175-
_ = PerformClearAsync().ContinueWith(t =>
176-
{
177-
if (t.IsFaulted && t.Exception != null)
178-
{
179-
Application.MainLoop?.Invoke(() =>
180-
{
181-
MessageBox.ErrorQuery("Clear Failed",
182-
t.Exception.InnerException?.Message ?? t.Exception.Message,
183-
"OK");
184-
});
185-
}
186-
}, TaskScheduler.Default);
187-
#pragma warning restore PPDS013
176+
_errorService?.FireAndForget(PerformClearAsync(), "ClearAllProfiles");
188177
}
189178

190179
private async Task PerformClearAsync()

src/PPDS.Cli/Tui/Dialogs/EnvironmentDetailsDialog.cs

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace PPDS.Cli.Tui.Dialogs;
1515
internal sealed class EnvironmentDetailsDialog : TuiDialog, ITuiStateCapture<EnvironmentDetailsDialogState>
1616
{
1717
private readonly InteractiveSession _session;
18+
private readonly ITuiErrorService _errorService;
1819
private readonly string _environmentUrl;
1920
private readonly string? _environmentDisplayName;
2021
private readonly ITuiThemeService _themeService;
@@ -44,6 +45,7 @@ public EnvironmentDetailsDialog(
4445
string? environmentDisplayName = null) : base("Environment Details", session)
4546
{
4647
_session = session ?? throw new ArgumentNullException(nameof(session));
48+
_errorService = session.GetErrorService();
4749
_environmentUrl = environmentUrl ?? throw new ArgumentNullException(nameof(environmentUrl));
4850
_environmentDisplayName = environmentDisplayName;
4951
_themeService = session.GetThemeService();
@@ -170,28 +172,7 @@ private void LoadDetailsAsync(CancellationToken cancellationToken)
170172
_refreshButton.Enabled = false;
171173
_statusLabel.Text = "Loading environment details...";
172174

173-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling
174-
_ = LoadDetailsInternalAsync(cancellationToken).ContinueWith(t =>
175-
{
176-
Application.MainLoop?.Invoke(() =>
177-
{
178-
// Check _disposed before accessing token (CTS may be disposed)
179-
if (_disposed)
180-
{
181-
return;
182-
}
183-
184-
_refreshButton.Enabled = true;
185-
186-
if (t.IsFaulted && t.Exception != null)
187-
{
188-
var message = t.Exception.InnerException?.Message ?? t.Exception.Message;
189-
_statusLabel.Text = $"Error: {message}";
190-
_statusLabel.ColorScheme = TuiColorPalette.Error;
191-
}
192-
});
193-
}, TaskScheduler.Default);
194-
#pragma warning restore PPDS013
175+
_errorService.FireAndForget(LoadDetailsInternalAsync(cancellationToken), "LoadEnvironmentDetails");
195176
}
196177

197178
private async Task LoadDetailsInternalAsync(CancellationToken cancellationToken)

src/PPDS.Cli/Tui/Dialogs/EnvironmentSelectorDialog.cs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal sealed class EnvironmentSelectorDialog : TuiDialog, ITuiStateCapture<En
1717
private readonly IEnvironmentService _environmentService;
1818
private readonly Action<DeviceCodeInfo>? _deviceCodeCallback;
1919
private readonly InteractiveSession? _session;
20+
private readonly ITuiErrorService? _errorService;
2021
private readonly TextField _filterField;
2122
private readonly ListView _listView;
2223
private readonly Label _statusLabel;
@@ -59,6 +60,7 @@ public EnvironmentSelectorDialog(
5960
_environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService));
6061
_deviceCodeCallback = deviceCodeCallback;
6162
_session = session;
63+
_errorService = session?.GetErrorService();
6264

6365
Width = 70;
6466
Height = 22;
@@ -177,21 +179,7 @@ public EnvironmentSelectorDialog(
177179
{
178180
_spinner.Start("Loading environments...");
179181

180-
// Discover environments asynchronously (fire-and-forget with error handling)
181-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
182-
_ = DiscoverEnvironmentsAsync().ContinueWith(t =>
183-
{
184-
if (t.IsFaulted && t.Exception != null)
185-
{
186-
Application.MainLoop?.Invoke(() =>
187-
{
188-
_spinner.Stop();
189-
_statusLabel.Text = $"Error: {t.Exception.InnerException?.Message ?? t.Exception.Message}";
190-
_statusLabel.Visible = true;
191-
});
192-
}
193-
}, TaskScheduler.Default);
194-
#pragma warning restore PPDS013
182+
_errorService?.FireAndForget(DiscoverEnvironmentsAsync(), "DiscoverEnvironments");
195183
};
196184
}
197185

src/PPDS.Cli/Tui/Dialogs/ExportDialog.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal sealed class ExportDialog : TuiDialog, ITuiStateCapture<ExportDialogSta
1717
{
1818
private static readonly IReadOnlyList<string> FormatNames = new[] { "CSV", "TSV", "JSON", "Clipboard" };
1919
private readonly IExportService _exportService;
20+
private readonly ITuiErrorService? _errorService;
2021
private readonly DataTable _dataTable;
2122
private readonly IReadOnlyDictionary<string, QueryColumnType>? _columnTypes;
2223
private readonly RadioGroup _formatGroup;
@@ -50,6 +51,7 @@ public ExportDialog(
5051
InteractiveSession? session = null) : base("Export Results", session)
5152
{
5253
_exportService = exportService ?? throw new ArgumentNullException(nameof(exportService));
54+
_errorService = session?.GetErrorService();
5355
_dataTable = dataTable ?? throw new ArgumentNullException(nameof(dataTable));
5456
_columnTypes = columnTypes;
5557

@@ -228,24 +230,7 @@ private void ExportToFile(int format, bool includeHeaders)
228230
_statusLabel.Text = "Exporting...";
229231
Application.Refresh();
230232

231-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling
232-
_ = ExportToFileAsync(filePath, format, includeHeaders).ContinueWith(t =>
233-
{
234-
Application.MainLoop?.Invoke(() =>
235-
{
236-
if (t.IsFaulted)
237-
{
238-
_statusLabel.Text = $"Error: {t.Exception?.InnerException?.Message ?? "Export failed"}";
239-
}
240-
else
241-
{
242-
_exportCompleted = true;
243-
MessageBox.Query("Export", $"Exported to:\n{filePath}", "OK");
244-
Application.RequestStop();
245-
}
246-
});
247-
}, TaskScheduler.Default);
248-
#pragma warning restore PPDS013
233+
_errorService?.FireAndForget(ExportToFileAsync(filePath, format, includeHeaders), "ExportToFile");
249234
}
250235

251236
private async Task ExportToFileAsync(string filePath, int format, bool includeHeaders)

src/PPDS.Cli/Tui/Dialogs/ProfileCreationDialog.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal sealed class ProfileCreationDialog : TuiDialog, ITuiStateCapture<Profil
3232
};
3333
private readonly IProfileService _profileService;
3434
private readonly IEnvironmentService _environmentService;
35+
private readonly ITuiErrorService? _errorService;
3536
private readonly Action<DeviceCodeInfo>? _deviceCodeCallback;
3637

3738
private readonly TextField _nameField;
@@ -87,6 +88,7 @@ public ProfileCreationDialog(
8788
{
8889
_profileService = profileService ?? throw new ArgumentNullException(nameof(profileService));
8990
_environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService));
91+
_errorService = session?.GetErrorService();
9092
_deviceCodeCallback = deviceCodeCallback;
9193

9294
Width = 70;
@@ -405,9 +407,7 @@ private void OnAuthenticateClicked()
405407
_statusLabel.Text = "Authenticating...";
406408
Application.Refresh();
407409

408-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling
409-
_ = CreateProfileAndHandleResultAsync(request, deviceCallback);
410-
#pragma warning restore PPDS013
410+
_errorService?.FireAndForget(CreateProfileAndHandleResultAsync(request, deviceCallback), "CreateProfile");
411411
}
412412

413413
private async Task CreateProfileAndHandleResultAsync(ProfileCreateRequest request, Action<DeviceCodeInfo>? deviceCodeCallback)

src/PPDS.Cli/Tui/Dialogs/ProfileDetailsDialog.cs

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -283,20 +283,7 @@ public ProfileDetailsDialog(InteractiveSession session) : base("Profile Details"
283283
refreshButton, closeButton
284284
);
285285

286-
// Load profile data asynchronously
287-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
288-
_ = LoadProfileAsync().ContinueWith(t =>
289-
{
290-
if (t.IsFaulted && t.Exception != null)
291-
{
292-
_errorService.ReportError("Failed to load profile details", t.Exception, "ProfileDetails");
293-
Application.MainLoop?.Invoke(() =>
294-
{
295-
_profileNameLabel.Text = "Error loading profile (see F12 for details)";
296-
});
297-
}
298-
}, TaskScheduler.Default);
299-
#pragma warning restore PPDS013
286+
_errorService.FireAndForget(LoadProfileAsync(), "LoadProfile");
300287
}
301288

302289
private async Task LoadProfileAsync()
@@ -450,15 +437,7 @@ private static string TruncateUrl(string url, int maxLength)
450437

451438
private void OnRefreshClicked()
452439
{
453-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
454-
_ = LoadProfileAsync().ContinueWith(t =>
455-
{
456-
if (t.IsFaulted && t.Exception != null)
457-
{
458-
_errorService.ReportError("Failed to refresh profile", t.Exception, "ProfileRefresh");
459-
}
460-
}, TaskScheduler.Default);
461-
#pragma warning restore PPDS013
440+
_errorService.FireAndForget(LoadProfileAsync(), "RefreshProfile");
462441
}
463442

464443
/// <inheritdoc />

src/PPDS.Cli/Tui/Dialogs/ProfileSelectorDialog.cs

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal sealed class ProfileSelectorDialog : TuiDialog, ITuiStateCapture<Profil
1515
{
1616
private readonly IProfileService _profileService;
1717
private readonly InteractiveSession? _session;
18+
private readonly ITuiErrorService? _errorService;
1819
private readonly ListView _listView;
1920
private readonly Label _detailLabel;
2021
private readonly TuiSpinner _spinner;
@@ -51,6 +52,7 @@ public ProfileSelectorDialog(IProfileService profileService, InteractiveSession?
5152
{
5253
_profileService = profileService ?? throw new ArgumentNullException(nameof(profileService));
5354
_session = session;
55+
_errorService = session?.GetErrorService();
5456

5557
Width = 60;
5658
Height = 19;
@@ -172,23 +174,7 @@ public ProfileSelectorDialog(IProfileService profileService, InteractiveSession?
172174
{
173175
_spinner.Start("Loading profiles...");
174176

175-
// Load profiles asynchronously (fire-and-forget with error handling)
176-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
177-
_ = LoadProfilesAsync().ContinueWith(t =>
178-
{
179-
if (t.IsFaulted && t.Exception != null)
180-
{
181-
Application.MainLoop?.Invoke(() =>
182-
{
183-
_spinner.Stop();
184-
_errorMessage = t.Exception.InnerException?.Message ?? t.Exception.Message;
185-
_detailLabel.Text = $"Error: {_errorMessage}";
186-
_detailLabel.Visible = true;
187-
_isLoading = false;
188-
});
189-
}
190-
}, TaskScheduler.Default);
191-
#pragma warning restore PPDS013
177+
_errorService?.FireAndForget(LoadProfilesAsync(), "LoadProfiles");
192178
};
193179
}
194180

@@ -391,20 +377,7 @@ private void ShowRenameDialog()
391377

392378
private void PerformRename(ProfileSummary profile, string newName)
393379
{
394-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling via ContinueWith
395-
_ = PerformRenameAsync(profile, newName).ContinueWith(t =>
396-
{
397-
if (t.IsFaulted && t.Exception != null)
398-
{
399-
var ex = t.Exception.InnerException;
400-
var message = ex is PpdsException ppdsEx ? ppdsEx.UserMessage : ex?.Message ?? "Unknown error";
401-
Application.MainLoop?.Invoke(() =>
402-
{
403-
MessageBox.ErrorQuery("Rename Failed", message, "OK");
404-
});
405-
}
406-
}, TaskScheduler.Default);
407-
#pragma warning restore PPDS013
380+
_errorService?.FireAndForget(PerformRenameAsync(profile, newName), "RenameProfile");
408381
}
409382

410383
private async Task PerformRenameAsync(ProfileSummary profile, string newName)
@@ -446,18 +419,7 @@ private void OnDeleteClicked()
446419

447420
if (result == 0) // "Delete" selected
448421
{
449-
#pragma warning disable PPDS013 // Fire-and-forget with explicit error handling
450-
_ = DeleteProfileAsync(profile).ContinueWith(t =>
451-
{
452-
if (t.IsFaulted)
453-
{
454-
Application.MainLoop?.Invoke(() =>
455-
{
456-
_detailLabel.Text = $"Error: {t.Exception?.InnerException?.Message ?? "Delete failed"}";
457-
});
458-
}
459-
}, TaskScheduler.Default);
460-
#pragma warning restore PPDS013
422+
_errorService?.FireAndForget(DeleteProfileAsync(profile), "DeleteProfile");
461423
}
462424
}
463425

0 commit comments

Comments
 (0)