code-quality batch (T-112)
test / unit + widget + golden + a11y (push) Failing after 31s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m7s

Seven small consultant findings, one commit:

1. TreeSitterLib stores last dlopen error + path in static fields
   instead of swallowing them. Callers that observe a null instance
   can now read the diagnostic.

2. Drop the Cmsghdr alias in libc.dart — back-compat shim with no
   callers; CLAUDE.md forbids those in a solo repo.

3. Drop EditorController._events field + the unused_field
   suppression. The constructor still subscribes via `events.on<...>`
   for _eventSub; the field itself was speculative retention.

4. Replace inline hex / errno literals in native_pty.dart with
   PosixErrno.{eintr,ebadf,epipe} and new libc.{pollin, pollAnyErr,
   sighup, sigkill, sigwinch}. PosixErrno gains eintr.

5. ExtensionManager records activate/deactivate exceptions in a
   `_failed` map exposed as `failedExtensions` + `didFail(id)`.
   Listeners are notified on entry/exit; cleared on a clean
   activate. UI surfaces the degraded state instead of pretending
   everything is fine.

6. file_tree_view imports FileEntry via the clide.dart barrel
   instead of `package:clide/src/files/listing.dart` directly — the
   leak the consultant flagged (barrel already re-exports it).

7. test_app branch in main.dart wrapped in `if (kDebugMode)` so
   release tree-shaker elides the test harness from shipping
   binaries. Source import stays; tree-shake handles the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 22:11:23 +02:00
co-authored by Claude Opus 4.7
parent b66e8f6cc0
commit d4f8f89016
11 changed files with 98 additions and 27 deletions
@@ -1803,3 +1803,5 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-105', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:56:28', '2026-05-17 19:56:28', '2026-05-17 19:56:28', NULL, '62dfdadc31b2c231afd837fb46f0a114', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-108', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:58:13', '2026-05-17 19:58:13', '2026-05-17 19:58:13', NULL, 'a5dbfe5881e152e82538181b0fe56609', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-108', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:05:38', '2026-05-17 20:05:38', '2026-05-17 20:05:38', NULL, '6815472fbfaa6d7740945b42d4c87f57', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:06:30', '2026-05-17 20:06:30', '2026-05-17 20:06:30', NULL, '52271781bf987a984d9f219956034d1f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:11:16', '2026-05-17 20:11:16', '2026-05-17 20:11:16', NULL, '6d51964a3ad7f1948f54a7c769336841', 1) ON CONFLICT(hash) DO NOTHING;
+11
View File
@@ -1788,3 +1788,14 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority,
2. Replace swallowed `onTimeout: () {}` with `onTimeout: () => fail(''...'')`.
Source: consultants.md "Tests — Findings — [Major]".', 'done', 'medium', NULL, NULL, NULL, '2026-05-17 18:47:59', '2026-05-17 20:05:38', NULL, '33e15d84caed686e88fc42f1f4a3586c', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'task', 'T-97', 'quick-wins: code-quality cleanups', 'Batch of small consultant findings that share a domain (code quality):
1. Replace silent `catch (_)` in `tree_sitter_ffi.dart:197,206` with a logged last-error `DynamicLibrary.open` failures are currently discarded.
2. Delete `Cmsghdr` alias in `libc.dart:201-202` ("backward compatibility" CLAUDE.md forbids in a solo repo).
3. Remove `// ignore: unused_field` in `editor_controller.dart:25` (speculative retention; fix not silence).
4. Consume `libc.dart` constants / `PosixErrno` in `native_pty.dart` instead of inline hex (`0x0001`, `28`, `4`, `9`).
5. `ExtensionManager.activate` (`extensions_manager.dart:141-143`) swallows exceptions surface a "degraded" state for the UI.
6. Verify `file_tree_view.dart:8` barrel leak (commit `5cad982` may already cover this).
7. Move `test_app.dart` out of production `main.dart` import graph (gate behind separate entrypoint or `kDebugMode`).
Source: consultants.md (Code Quality + Architecture Findings Minor).', 'done', 'low', NULL, NULL, NULL, '2026-05-17 18:48:20', '2026-05-17 20:11:16', NULL, 'fccc783ff24be83f6d5a498e936c4af9', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+5
View File
@@ -59,6 +59,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
instead of fixed sleeps; `onTimeout` callbacks now `fail()` loudly
with diagnostic context. `RecordingEventSink` exposes a broadcast
stream for the same pattern (T-108).
- Code-quality cleanups — `TreeSitterLib` exposes a last-error
diagnostic instead of swallowing dlopen failures; `ExtensionManager`
surfaces a `failedExtensions` map for UI degradation; PTY constants
consolidated in `libc.dart` + `PosixErrno`; `test_app.dart` gated
behind `kDebugMode` (T-112).
- Test sweep — `keybindings`, `toolchain_paths`, and several
`widgets/src/` primitives (tooltip, palette, multitab, markdown).
- `tree_sitter_service` sweep — fake-FFI + real-library smoke,
@@ -17,13 +17,11 @@ import 'package:clide/kernel/kernel.dart';
import 'package:flutter/foundation.dart';
class EditorController extends ChangeNotifier {
EditorController({required this.ipc, required DaemonBus events}) : _events = events {
EditorController({required this.ipc, required DaemonBus events}) {
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
}
final DaemonClient ipc;
// ignore: unused_field — kept for future subscription changes
final DaemonBus _events;
StreamSubscription<DaemonEvent>? _eventSub;
+1 -2
View File
@@ -1,12 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:clide/clide.dart' show FileEntry;
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
import 'package:clide/src/files/listing.dart' show FileEntry;
import 'file_tree_controller.dart';
/// Sidebar panel rendering the workspace file tree.
+14
View File
@@ -81,6 +81,9 @@ class ExtensionManager extends ChangeNotifier {
final Map<String, ClideExtension> _known = {};
final Set<String> _activated = {};
// Extensions whose activate() / deactivate() threw — exposed so the
// UI can show a "degraded" status next to them.
final Map<String, Object> _failed = {};
void register(ClideExtension ext) {
if (_known.containsKey(ext.id)) {
@@ -94,6 +97,12 @@ class ExtensionManager extends ChangeNotifier {
Iterable<ClideExtension> get all => _known.values;
bool isActivated(String id) => _activated.contains(id);
/// Map of extension id → most recent activate/deactivate error.
/// Cleared for an extension when it activates cleanly. UI surfaces
/// read this for the "degraded" indicator.
Map<String, Object> get failedExtensions => Map.unmodifiable(_failed);
bool didFail(String id) => _failed.containsKey(id);
bool isEnabled(String id) {
final v = settings.get<bool>('app.extensions.$id.enabled');
return v ?? true;
@@ -138,11 +147,14 @@ class ExtensionManager extends ChangeNotifier {
_applyContribution(c);
}
_activated.add(id);
_failed.remove(id);
events.emit(ExtensionActivated(id: id));
notifyListeners();
log.info('extensions', 'activated $id');
} catch (e, st) {
_failed[id] = e;
log.error('extensions', 'activate failed for $id', error: e, stackTrace: st);
notifyListeners();
}
}
@@ -160,7 +172,9 @@ class ExtensionManager extends ChangeNotifier {
notifyListeners();
log.info('extensions', 'deactivated $id');
} catch (e, st) {
_failed[id] = e;
log.error('extensions', 'deactivate failed for $id', error: e, stackTrace: st);
notifyListeners();
}
}
+30 -5
View File
@@ -243,6 +243,15 @@ class TreeSitterLib {
@visibleForTesting
static TreeSitterLib fromDynamicLibrary(DynamicLibrary lib) => TreeSitterLib._(lib);
/// Most recent error raised while trying to dlopen the tree-sitter
/// library, paired with the path that was attempted. Cleared on a
/// successful open. Null means "no attempt failed yet."
///
/// Callers that observe a null `instance` after `init()` should read
/// this for the diagnostic instead of guessing.
static Object? lastOpenError;
static String? lastOpenErrorPath;
static DynamicLibrary? _openLibrary() {
final libName = Platform.isLinux
? 'libtree-sitter.so'
@@ -251,12 +260,22 @@ class TreeSitterLib {
: Platform.isWindows
? 'tree-sitter.dll'
: null;
if (libName == null) return null;
if (libName == null) {
lastOpenError = 'unsupported platform ${Platform.operatingSystem}';
lastOpenErrorPath = null;
return null;
}
// Try standard dlopen path first (works when lib is in bundle/lib/).
try {
return DynamicLibrary.open(libName);
} catch (_) {}
final lib = DynamicLibrary.open(libName);
lastOpenError = null;
lastOpenErrorPath = null;
return lib;
} catch (e) {
lastOpenError = e;
lastOpenErrorPath = libName;
}
// Try next to executable.
final exe = File(Platform.resolvedExecutable).parent.path;
@@ -264,8 +283,14 @@ class TreeSitterLib {
final path = '$dir/$libName';
if (File(path).existsSync()) {
try {
return DynamicLibrary.open(path);
} catch (_) {}
final lib = DynamicLibrary.open(path);
lastOpenError = null;
lastOpenErrorPath = null;
return lib;
} catch (e) {
lastOpenError = e;
lastOpenErrorPath = path;
}
}
}
return null;
+9 -4
View File
@@ -48,10 +48,15 @@ Future<void> main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
// Test mode: skip the full app, run the test harness instead.
const testMode = String.fromEnvironment('CLIDE_TESTMODE');
if (testMode.isNotEmpty) {
runApp(const ClideTestApp());
return;
// Gated on kDebugMode so the release tree-shaker can elide both the
// branch and the test_app import graph — production binaries don't
// ship the harness.
if (kDebugMode) {
const testMode = String.fromEnvironment('CLIDE_TESTMODE');
if (testMode.isNotEmpty) {
runApp(const ClideTestApp());
return;
}
}
binding.ensureSemantics();
+1
View File
@@ -15,6 +15,7 @@ abstract class PosixErrno {
static const int eperm = 1;
static const int enoent = 2;
static const int esrch = 3;
static const int eintr = 4;
static const int eio = 5;
static const int ebadf = 9;
static const int eagain = 11;
+13 -3
View File
@@ -47,6 +47,19 @@ const int fSetFl = 4;
final int tiocswinsz = Platform.isMacOS ? 0x80087467 : 0x5414;
// poll() event bits (POSIX — same numeric values on Linux + macOS).
const int pollin = 0x0001;
const int pollerr = 0x0008;
const int pollhup = 0x0010;
const int pollnval = 0x0020;
const int pollAnyErr = pollerr | pollhup | pollnval;
// Signal numbers used from the PTY layer (POSIX standard; identical
// across Linux + macOS for the entries we touch).
const int sighup = 1;
const int sigkill = 9;
const int sigwinch = 28;
// ---------------------------------------------------------------------------
// Typedefs
// ---------------------------------------------------------------------------
@@ -198,9 +211,6 @@ final class CmsghdrDarwin extends ffi.Struct {
external int cmsg_type;
}
// Alias for backward compatibility — callers use Cmsghdr.
typedef Cmsghdr = CmsghdrLinux;
/// POSIX `struct winsize` for `TIOCSWINSZ`.
final class Winsize extends ffi.Struct {
@ffi.Uint16()
+11 -10
View File
@@ -25,6 +25,7 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'errors.dart';
import '../ipc/errno_mapping.dart' show PosixErrno;
import 'ffi/libc.dart' as libc;
// -- structs ----------------------------------------------------------------
@@ -107,7 +108,6 @@ final int _kSpawnSetsid = Platform.isMacOS ? 0x400 : 0x80;
// platform layout (glibc posix_spawnattr_t is 336 B; macOS even smaller).
const int _kSpawnStructBytes = 8192;
const _kSighup = 1;
const _kWnohang = 1;
// -- NativePty --------------------------------------------------------------
@@ -343,14 +343,16 @@ class NativePty {
final buf = malloc<ffi.Uint8>(65536);
final pfd = calloc<_Pollfd>();
pfd.ref.fd = fd;
pfd.ref.events = 0x0001; // POLLIN
pfd.ref.events = libc.pollin;
try {
while (true) {
final ready = poll(pfd, 1, 100);
if (ready < 0) break;
if (ready == 0) continue;
if (pfd.ref.revents & 0x0038 != 0 && pfd.ref.revents & 0x0001 == 0) {
// Slave closed (POLLHUP / POLLERR / POLLNVAL) with no buffered
// bytes left to read — caller loop exits and we send EOF.
if (pfd.ref.revents & libc.pollAnyErr != 0 && pfd.ref.revents & libc.pollin == 0) {
break;
}
final n = rd(fd, buf.cast(), 65536);
@@ -383,8 +385,8 @@ class NativePty {
);
if (n < 0) {
final err = libc.errno;
if (err == 4 /* EINTR */) continue;
if (err == 9 /* EBADF */ || err == 32 /* EPIPE */) _dead = true;
if (err == PosixErrno.eintr) continue;
if (err == PosixErrno.ebadf || err == PosixErrno.epipe) _dead = true;
throw PtyException('write', 'write to PTY failed', errno: err);
}
if (n == 0) break;
@@ -405,17 +407,16 @@ class NativePty {
..ref.wsCol = cols;
final rc = _ioctl(_fd, _kTiocsWinsz, ws);
calloc.free(ws);
if (rc < 0 && libc.errno == 9 /* EBADF */) {
if (rc < 0 && libc.errno == PosixErrno.ebadf) {
_dead = true;
return;
}
// Explicitly signal the child to re-query its terminal size.
// SIGWINCH = 28 on both macOS and Linux.
_nativeKill(pid, 28);
_nativeKill(pid, libc.sigwinch);
}
/// Send a signal to the child.
bool kill([int signal = _kSighup]) {
bool kill([int signal = libc.sighup]) {
if (_dead) return false;
return _nativeKill(pid, signal) == 0;
}
@@ -444,7 +445,7 @@ class NativePty {
// otherwise close() racing with start() leaves an orphan isolate.
await _readerReady;
_nativeKill(pid, _kSighup);
_nativeKill(pid, libc.sighup);
_nativeKill(pid, 9);
// Wait for the isolate to send `null` (EOF) — confirms it has