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
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:
@@ -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,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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user