tree_sitter test coverage + DI seam, ratchet floor to 95
Add `colorForRole` switch-arm tests (every role → token mapping plus the unknown-role fallback). Introduce a DI seam in `TreeSitterService` and `TreeSitterLib` so tests can substitute the FFI surface and asset loaders without dlopen'ing `libtree-sitter.so` — `TreeSitterLib.testing(...)` takes named per-function overrides with safe no-op defaults, and `TreeSitterLib.fromDynamicLibrary(...)` lets the smoke test load the vendored library explicitly. Production paths (`TreeSitterService.shared`, `TreeSitterLib.instance`) are unchanged. Fake-FFI tests walk every branch of `_init`, `_loadGrammar`, `highlight`, and `dispose`. The smoke test catches FFI-signature regressions the fakes can't, by exercising the real native library end-to-end on Linux. Together this takes `tree_sitter_service.dart` from 17% to 96% and crosses the global 95% target — closing out the D-66 line-coverage epic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'dart:ffi';
|
||||
import 'dart:io' show File, Platform;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// -- Opaque handles ----------------------------------------------------------
|
||||
|
||||
@@ -146,6 +147,60 @@ class TreeSitterLib {
|
||||
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
|
||||
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
|
||||
|
||||
/// Constructs a [TreeSitterLib] from caller-supplied Dart closures. Used by
|
||||
/// tests to substitute the FFI surface without dlopen'ing the real library;
|
||||
/// each unspecified function defaults to a safe no-op (pointers return
|
||||
/// `nullptr`, ints return `0`, bools return `false`). Tests override the
|
||||
/// few entries they exercise.
|
||||
@visibleForTesting
|
||||
TreeSitterLib.testing({
|
||||
DTsParserNew? parserNew,
|
||||
DTsParserDelete? parserDelete,
|
||||
DTsParserSetLanguage? parserSetLanguage,
|
||||
DTsParserSetWasmStore? parserSetWasmStore,
|
||||
DTsParserParseString? parserParseString,
|
||||
DTsTreeDelete? treeDelete,
|
||||
DTsTreeRootNode? treeRootNode,
|
||||
DTsNodeStartByte? nodeStartByte,
|
||||
DTsNodeEndByte? nodeEndByte,
|
||||
DTsQueryNew? queryNew,
|
||||
DTsQueryDelete? queryDelete,
|
||||
DTsQueryCaptureCount? queryCaptureCount,
|
||||
DTsQueryCaptureNameForId? queryCaptureNameForId,
|
||||
DTsQueryCursorNew? queryCursorNew,
|
||||
DTsQueryCursorDelete? queryCursorDelete,
|
||||
DTsQueryCursorExec? queryCursorExec,
|
||||
DTsQueryCursorNextMatch? queryCursorNextMatch,
|
||||
DTsWasmStoreNew? wasmStoreNew,
|
||||
DTsWasmStoreDelete? wasmStoreDelete,
|
||||
DTsWasmStoreLoadLanguage? wasmStoreLoadLanguage,
|
||||
DWasmEngineNew? wasmEngineNew,
|
||||
DWasmEngineDelete? wasmEngineDelete,
|
||||
}) : parserNew = parserNew ?? (() => nullptr),
|
||||
parserDelete = parserDelete ?? ((_) {}),
|
||||
parserSetLanguage = parserSetLanguage ?? ((_, __) => false),
|
||||
parserSetWasmStore = parserSetWasmStore ?? ((_, __) {}),
|
||||
parserParseString = parserParseString ?? ((_, __, ___, ____) => nullptr),
|
||||
treeDelete = treeDelete ?? ((_) {}),
|
||||
// Leaks a zeroed TSNode allocation — only hit when the test supplies
|
||||
// a non-null parserParseString without also supplying treeRootNode.
|
||||
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
|
||||
nodeStartByte = nodeStartByte ?? ((_) => 0),
|
||||
nodeEndByte = nodeEndByte ?? ((_) => 0),
|
||||
queryNew = queryNew ?? ((_, __, ___, ____, _____) => nullptr),
|
||||
queryDelete = queryDelete ?? ((_) {}),
|
||||
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
|
||||
queryCaptureNameForId = queryCaptureNameForId ?? ((_, __, ___) => nullptr),
|
||||
queryCursorNew = queryCursorNew ?? (() => nullptr),
|
||||
queryCursorDelete = queryCursorDelete ?? ((_) {}),
|
||||
queryCursorExec = queryCursorExec ?? ((_, __, ___) {}),
|
||||
queryCursorNextMatch = queryCursorNextMatch ?? ((_, __) => false),
|
||||
wasmStoreNew = wasmStoreNew ?? ((_, __) => nullptr),
|
||||
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
|
||||
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, __, ___, ____, _____) => nullptr),
|
||||
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
|
||||
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
|
||||
|
||||
final DTsParserNew parserNew;
|
||||
final DTsParserDelete parserDelete;
|
||||
final DTsParserSetLanguage parserSetLanguage;
|
||||
@@ -181,6 +236,13 @@ class TreeSitterLib {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Constructs a [TreeSitterLib] from an already-loaded [DynamicLibrary].
|
||||
/// Used by the smoke test to dlopen the vendored `libtree-sitter.so`
|
||||
/// directly without going through the global `init()` / `_instance`
|
||||
/// dance, so the test stays isolated from the singleton.
|
||||
@visibleForTesting
|
||||
static TreeSitterLib fromDynamicLibrary(DynamicLibrary lib) => TreeSitterLib._(lib);
|
||||
|
||||
static DynamicLibrary? _openLibrary() {
|
||||
final libName = Platform.isLinux
|
||||
? 'libtree-sitter.so'
|
||||
|
||||
@@ -8,8 +8,17 @@ import 'package:clide/kernel/src/syntax/language_map.dart';
|
||||
import 'package:clide/kernel/src/syntax/tree_sitter_ffi.dart';
|
||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
/// Loads grammar WASM bytes for [language] (e.g. "dart" → `dart.wasm`).
|
||||
/// Throws on missing or unreadable assets.
|
||||
typedef GrammarBytesLoader = Future<Uint8List> Function(String language);
|
||||
|
||||
/// Loads the highlight query (`.scm` source) for [language], or returns
|
||||
/// null if no query is bundled for it.
|
||||
typedef GrammarQueryLoader = Future<String?> Function(String language);
|
||||
|
||||
class SyntaxSpan {
|
||||
const SyntaxSpan({
|
||||
required this.start,
|
||||
@@ -42,8 +51,37 @@ class _LoadedGrammar {
|
||||
}
|
||||
|
||||
class TreeSitterService {
|
||||
static final TreeSitterService shared = TreeSitterService._();
|
||||
TreeSitterService._();
|
||||
static final TreeSitterService shared = TreeSitterService();
|
||||
|
||||
/// Production constructor: uses the dlopen'd [TreeSitterLib.instance] and
|
||||
/// the Flutter [rootBundle]. Tests pass [lib] / [grammarBytes] /
|
||||
/// [grammarQuery] to substitute a fake FFI surface and in-memory assets.
|
||||
TreeSitterService({
|
||||
TreeSitterLib? lib,
|
||||
GrammarBytesLoader? grammarBytes,
|
||||
GrammarQueryLoader? grammarQuery,
|
||||
}) : _injectedLib = lib,
|
||||
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
|
||||
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
|
||||
|
||||
final TreeSitterLib? _injectedLib;
|
||||
final GrammarBytesLoader _grammarBytes;
|
||||
final GrammarQueryLoader _grammarQuery;
|
||||
|
||||
TreeSitterLib? get _lib => _injectedLib ?? TreeSitterLib.instance;
|
||||
|
||||
static Future<Uint8List> _defaultGrammarBytes(String language) async {
|
||||
final data = await rootBundle.load('assets/grammars/$language.wasm');
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
static Future<String?> _defaultGrammarQuery(String language) async {
|
||||
try {
|
||||
return await rootBundle.loadString('assets/queries/$language.scm');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, _LoadedGrammar> _grammars = {};
|
||||
final Set<String> _unavailable = {};
|
||||
@@ -58,7 +96,7 @@ class TreeSitterService {
|
||||
if (_initDone) return _parser != null;
|
||||
_initDone = true;
|
||||
|
||||
final lib = TreeSitterLib.instance;
|
||||
final lib = _lib;
|
||||
if (lib == null) return false;
|
||||
|
||||
final engine = lib.wasmEngineNew();
|
||||
@@ -92,12 +130,11 @@ class TreeSitterService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final lib = TreeSitterLib.instance!;
|
||||
final lib = _lib!;
|
||||
|
||||
try {
|
||||
// Load grammar WASM bytes.
|
||||
final wasmData = await rootBundle.load('assets/grammars/$language.wasm');
|
||||
final wasmBytes = wasmData.buffer.asUint8List();
|
||||
final wasmBytes = await _grammarBytes(language);
|
||||
|
||||
// Load into WASM store.
|
||||
final nameNative = language.toNativeUtf8();
|
||||
@@ -126,10 +163,7 @@ class TreeSitterService {
|
||||
calloc.free(error);
|
||||
|
||||
// Load highlight query.
|
||||
String? querySource;
|
||||
try {
|
||||
querySource = await rootBundle.loadString('assets/queries/$language.scm');
|
||||
} catch (_) {}
|
||||
final querySource = await _grammarQuery(language);
|
||||
|
||||
Pointer<TSQuery> query = nullptr;
|
||||
List<String> captureNames = [];
|
||||
@@ -200,7 +234,7 @@ class TreeSitterService {
|
||||
return SyntaxResult.empty;
|
||||
}
|
||||
|
||||
final lib = TreeSitterLib.instance!;
|
||||
final lib = _lib!;
|
||||
final parser = _parser!;
|
||||
final cursor = _cursor!;
|
||||
|
||||
@@ -253,7 +287,7 @@ class TreeSitterService {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
final lib = TreeSitterLib.instance;
|
||||
final lib = _lib;
|
||||
if (lib == null) return;
|
||||
|
||||
for (final grammar in _grammars.values) {
|
||||
@@ -273,6 +307,15 @@ class TreeSitterService {
|
||||
_unavailable.clear();
|
||||
}
|
||||
|
||||
/// Resets the service to a pre-init state. Tests use this to re-exercise
|
||||
/// `_init()` without constructing a new singleton; production code never
|
||||
/// needs it.
|
||||
@visibleForTesting
|
||||
void resetForTests() {
|
||||
dispose();
|
||||
_initDone = false;
|
||||
}
|
||||
|
||||
static Color colorForRole(String role, SurfaceTokens tokens) {
|
||||
return switch (role) {
|
||||
'keyword' || 'repeat' || 'conditional' || 'include' || 'exception' || 'operator' => tokens.syntaxKeyword,
|
||||
|
||||
Reference in New Issue
Block a user