diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 71a52380..29a93e84 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:flutter/widgets.dart'; import 'dart:async' show Timer; diff --git a/lib/features/connections/connections_panel_pg_tree.dart b/lib/features/connections/connections_panel_pg_tree.dart index 1a2a30f9..238a6200 100644 --- a/lib/features/connections/connections_panel_pg_tree.dart +++ b/lib/features/connections/connections_panel_pg_tree.dart @@ -425,7 +425,8 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> { List matviews = []; try { matviews = await conn.listMaterializedViews(schema: widget.schemaName); - } catch (_) { + } catch (e, st) { + logIgnored('PgTree.materializedViews', e, st); // pg_matviews / permissions may fail on some servers; keep tree usable. } final functions = await conn.listFunctions(schema: widget.schemaName); diff --git a/lib/features/connections/extension_connection_form.dart b/lib/features/connections/extension_connection_form.dart index 788cb125..5e177a98 100644 --- a/lib/features/connections/extension_connection_form.dart +++ b/lib/features/connections/extension_connection_form.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:querya_desktop/core/database/database_error_mapper.dart'; import 'package:flutter/widgets.dart'; @@ -327,7 +328,8 @@ Map _sduiInitialValuesFromConnection(ConnectionRow row) { values[key] = entry.value; } } - } catch (_) { + } catch (e, st) { + logIgnored('ExtensionConnectionForm.driverOptions', e, st); // Ignore malformed driverOptions; host fields still apply. } } diff --git a/lib/features/connections/sqlite_connection_form.dart b/lib/features/connections/sqlite_connection_form.dart index 1f450415..a2607a07 100644 --- a/lib/features/connections/sqlite_connection_form.dart +++ b/lib/features/connections/sqlite_connection_form.dart @@ -140,7 +140,16 @@ class _SqliteConnectionFormContentState if (_nameController.text.isEmpty) { _nameController.text = p.basenameWithoutExtension(path); } - } catch (_) {} + } catch (e, st) { + if (!mounted) return; + reportError( + context, + e, + stack: st, + where: 'SqliteConnectionForm.pickFile', + action: 'The file could not be chosen', + ); + } } Future _testConnection() async { diff --git a/lib/features/erd/erd_view.dart b/lib/features/erd/erd_view.dart index 6c79285a..432f7b6c 100644 --- a/lib/features/erd/erd_view.dart +++ b/lib/features/erd/erd_view.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'dart:async'; import 'dart:math' show max; import 'dart:typed_data'; @@ -616,7 +617,8 @@ class _ErdViewState extends material.State _captureCurrent(); try { await widget.layoutStore!.write(widget.layoutKey!, _c.toStore()); - } catch (_) { + } catch (e, st) { + logIgnored('ErdView.saveLayout', e, st); // Keeping the layout is a convenience: a failed write is not an error // the user can act on. } diff --git a/lib/features/extensions/extension_table_view.dart b/lib/features/extensions/extension_table_view.dart index 9196519b..71c49d36 100644 --- a/lib/features/extensions/extension_table_view.dart +++ b/lib/features/extensions/extension_table_view.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:querya_desktop/core/database/database_error_mapper.dart'; import 'package:flutter/widgets.dart'; @@ -125,7 +126,9 @@ class ExtensionTableDataDelegate extends TableDataMutationDelegate { if (countResult.rows.isNotEmpty && countResult.rows.first.isNotEmpty) { totalRows = int.tryParse(countResult.rows.first.first); } - } catch (_) {} + } catch (e, st) { + logIgnored('ExtensionTableView.count fallback', e, st); + } } } diff --git a/lib/features/main_screen/main_screen_dialogs.dart b/lib/features/main_screen/main_screen_dialogs.dart index a64208dc..941d5750 100644 --- a/lib/features/main_screen/main_screen_dialogs.dart +++ b/lib/features/main_screen/main_screen_dialogs.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/shared/report_error.dart'; import 'package:flutter/widgets.dart'; import 'dart:async'; @@ -140,7 +141,18 @@ class MainScreenDialogs { await connectionsPanelKey.currentState?.reloadConnectionsFromDb(); workspace.value = workspace.value.selectConnection(demoConn); nav.onSqliteOpenSqlWorkspace(demoConn); - } catch (_) {} + } catch (e, st) { + // A silent nothing after the user asked for the demo reads as a dead + // button. + if (!_mounted()) return; + reportError( + _context(), + e, + stack: st, + where: 'MainScreenDialogs.onLaunchDemoPlayground', + action: 'The demo playground could not be opened', + ); + } } Future handlePendingFileLaunch() async { diff --git a/lib/features/mongodb/mongo_databases_view.dart b/lib/features/mongodb/mongo_databases_view.dart index 94c1818a..ce9da2ec 100644 --- a/lib/features/mongodb/mongo_databases_view.dart +++ b/lib/features/mongodb/mongo_databases_view.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:querya_desktop/core/database/database_error_mapper.dart'; import 'package:flutter/widgets.dart'; @@ -116,7 +117,9 @@ class _MongoDatabasesViewState extends State { 'admin', {'listDatabases': 1}, ); - } catch (_) {} + } catch (e, st) { + logIgnored('MongoDatabasesView.listDatabases sizes', e, st); + } final dbList = dbListResult?['databases'] as List? ?? []; diff --git a/lib/features/mongodb/mongo_documents_view.dart b/lib/features/mongodb/mongo_documents_view.dart index e0b19e8a..56cbdd26 100644 --- a/lib/features/mongodb/mongo_documents_view.dart +++ b/lib/features/mongodb/mongo_documents_view.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:querya_desktop/core/database/database_error_mapper.dart'; import 'package:flutter/widgets.dart'; @@ -138,7 +139,8 @@ class _MongoDocumentsViewState extends material.State { _viewMode = MongoDocumentsViewMode.fromString(saved); }); } - } catch (_) { + } catch (e, st) { + logIgnored('MongoDocumentsView.viewMode', e, st); // Keep current mode } } @@ -227,7 +229,8 @@ class _MongoDocumentsViewState extends material.State { ); if (!mounted || seq != _loadSeq) return; setState(() => _totalCount = count); - } catch (_) { + } catch (e, st) { + logIgnored('MongoDocumentsView.count', e, st); // Keep the "maybe more" pagination. } } diff --git a/lib/features/mongodb/mongo_query_workspace.dart b/lib/features/mongodb/mongo_query_workspace.dart index b54143b5..8a4d3c32 100644 --- a/lib/features/mongodb/mongo_query_workspace.dart +++ b/lib/features/mongodb/mongo_query_workspace.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:flutter/widgets.dart'; import 'dart:async' show unawaited; @@ -203,7 +204,8 @@ class _MongoQueryWorkspaceState extends material.State { databaseName: widget.database, sqlText: query, ); - } catch (_) { + } catch (e, st) { + logIgnored('MongoQueryWorkspace.history', e, st); // Best-effort history logging } } diff --git a/lib/features/redis/redis_cli_workspace.dart b/lib/features/redis/redis_cli_workspace.dart index 560c64d0..bb56367f 100644 --- a/lib/features/redis/redis_cli_workspace.dart +++ b/lib/features/redis/redis_cli_workspace.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:flutter/widgets.dart'; import 'dart:async' show unawaited; import 'dart:convert'; @@ -287,7 +288,9 @@ class _RedisCliWorkspaceState extends material.State { }); } } - } catch (_) {} + } catch (e, st) { + logIgnored('RedisCliWorkspace.loadHistory', e, st); + } } Future _saveHistory() async { @@ -299,7 +302,9 @@ class _RedisCliWorkspaceState extends material.State { _historyStorageKey, jsonEncode(capped), ); - } catch (_) {} + } catch (e, st) { + logIgnored('RedisCliWorkspace.saveHistory', e, st); + } } @override diff --git a/lib/features/redis/redis_key_editor.dart b/lib/features/redis/redis_key_editor.dart index 37ca2cfc..b4af8e18 100644 --- a/lib/features/redis/redis_key_editor.dart +++ b/lib/features/redis/redis_key_editor.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:querya_desktop/core/database/database_error_mapper.dart'; import 'package:flutter/widgets.dart'; @@ -520,7 +521,9 @@ class _RedisKeyEditorState extends material.State { } catch (e) { try { await widget.connection.lrem(_cmdKey, 1, sentinel); - } catch (_) {} + } catch (cleanupError, cleanupStack) { + logIgnored('RedisKeyEditor.cleanup sentinel', cleanupError, cleanupStack); + } rethrow; } await _load(); diff --git a/lib/features/settings/preferences_mcp_section.dart b/lib/features/settings/preferences_mcp_section.dart index 05d5cdce..fe4a6863 100644 --- a/lib/features/settings/preferences_mcp_section.dart +++ b/lib/features/settings/preferences_mcp_section.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:querya_desktop/shared/report_error.dart'; import 'package:flutter/widgets.dart'; import 'dart:async' show StreamSubscription, unawaited; @@ -89,7 +90,9 @@ class PreferencesMcpSectionState extends material.State { try { activity = await (widget.loadActivity ?? () => LocalDb.instance.listMcpActivity(limit: 50))(); - } catch (_) {} + } catch (e, st) { + logIgnored('PreferencesMcpSection.load activity', e, st); + } if (!mounted) return; setState(() { _enabled = enabled; @@ -112,7 +115,9 @@ class PreferencesMcpSectionState extends material.State { final list = await (widget.loadActivity ?? () => LocalDb.instance.listMcpActivity(limit: 50))(); if (mounted) setState(() => _activity = list); - } catch (_) {} + } catch (e, st) { + logIgnored('PreferencesMcpSection.loadActivity', e, st); + } } Future _setEnabled(bool v) async { diff --git a/lib/features/sqlite/sqlite_overview_tab.dart b/lib/features/sqlite/sqlite_overview_tab.dart index f8c0bcf4..3c6cde96 100644 --- a/lib/features/sqlite/sqlite_overview_tab.dart +++ b/lib/features/sqlite/sqlite_overview_tab.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'package:flutter/widgets.dart'; import 'dart:io'; import 'dart:math' as math; @@ -129,7 +130,9 @@ class _SqliteOverviewTabState extends material.State { return f.lengthSync(); } } - } catch (_) {} + } catch (e, st) { + logIgnored('SqliteOverviewTab.fileSize', e, st); + } return 0; } diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 945fb55b..6012b9af 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -1,3 +1,4 @@ +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'dart:async'; import 'package:flutter/material.dart' as material; @@ -186,7 +187,9 @@ class SqliteSqlExecutionDelegate extends BaseSqlExecutionDelegate { [target.tableName], ); isView = kind.isNotEmpty && kind.first['type'] == 'view'; - } catch (_) {} + } catch (e, st) { + logIgnored('SqliteSqlWorkspace.isView probe', e, st); + } return SqlResultGridSchema.fromLoad( await loadTableViewSchema( diff --git a/lib/shared/services/data_export_service.dart b/lib/shared/services/data_export_service.dart index 66f341f6..b084e10b 100644 --- a/lib/shared/services/data_export_service.dart +++ b/lib/shared/services/data_export_service.dart @@ -1,3 +1,5 @@ +import 'package:querya_desktop/core/util/querya_log.dart'; +import 'package:querya_desktop/core/util/log_ignored.dart'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; @@ -459,16 +461,23 @@ class DataExportService { await sink.close(); sink = null; return SaveExportOutcome.written; - } on Object { + } on Object catch (error, stack) { + // The caller gets the outcome; the reason goes to the log. + QueryaLog.instance.error('DataExportService', 'export to a file failed', + error: error, stack: stack); try { await sink?.close(); - } catch (_) {} + } catch (e, st) { + logIgnored('DataExportService.closeSink', e, st); + } try { final partial = File(path); if (await partial.exists()) { await partial.delete(); } - } catch (_) {} + } catch (e, st) { + logIgnored('DataExportService.deletePartial', e, st); + } return SaveExportOutcome.error; } } diff --git a/test/guards/no_silent_catch_test.dart b/test/guards/no_silent_catch_test.dart new file mode 100644 index 00000000..1b240dde --- /dev/null +++ b/test/guards/no_silent_catch_test.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// Error-handling epic (#1764, #1763): a `catch` in the UI layer must do +/// something with the error: `logIgnored`, the reporting helpers, a rethrow or +/// a real fallback. An empty block (or one that holds only a comment) hides a +/// failure from both the user and the log. +/// +/// The files below keep a few empty blocks on purpose: they try to parse a +/// value in a second form and fall back, so the failure is the control flow. +/// The number is exact, so removing one of them has to lower it here too. +void main() { + const allowed = { + 'lib/features/mongodb/mongo_field_codec.dart': 1, + 'lib/features/mongodb/mql_parser.dart': 1, + 'lib/features/workspace/data_grid_value_panel.dart': 6, + }; + + final emptyCatch = RegExp( + r'catch\s*\([^)]*\)\s*\{(?:\s|//[^\n]*|/\*[\s\S]*?\*/)*\}', + ); + + String norm(File f) => f.path.replaceAll(r'\', '/'); + + test('lib/features and lib/shared have no empty catch blocks', () { + final counts = {}; + for (final dir in ['lib/features', 'lib/shared']) { + for (final e in Directory(dir).listSync(recursive: true)) { + if (e is! File || !e.path.endsWith('.dart')) continue; + final n = emptyCatch.allMatches(e.readAsStringSync()).length; + if (n > 0) counts[norm(e)] = n; + } + } + + final problems = []; + for (final entry in counts.entries) { + final expected = allowed[entry.key] ?? 0; + if (entry.value > expected) { + problems.add('${entry.key}: ${entry.value} empty catch blocks ' + '(allowed $expected): call logIgnored(where, e, st) or report it'); + } + } + for (final entry in allowed.entries) { + final actual = counts[entry.key] ?? 0; + if (actual < entry.value) { + problems.add('${entry.key}: ${entry.value} allowed but only $actual ' + 'left: lower the number in this test'); + } + } + + expect(problems, isEmpty, reason: problems.join('\n')); + }); +}