201 lines
8.4 KiB
Zig
201 lines
8.4 KiB
Zig
//! Shared comptime contract-validation helpers.
|
|
//!
|
|
//! Both the TUI tab framework (`src/tui/tab_framework.zig`) and the CLI
|
|
//! command framework (`src/commands/framework.zig`) walk a candidate
|
|
//! module at comptime to assert it conforms to the framework contract.
|
|
//! The check shapes are identical:
|
|
//!
|
|
//! - `expectDeclWithType` — a non-fn decl exists with the exact type.
|
|
//! - `expectFn` — a fn decl exists with the exact fn-pointer type.
|
|
//! - `expectFnInferredError` — a fn decl exists, params match, and
|
|
//! the return is an error union ending in the expected payload
|
|
//! (the error set itself is not checked because Zig infers an
|
|
//! empty error set per fn for `pub fn foo() !void`).
|
|
//!
|
|
//! Each helper takes a caller-supplied `kind` prefix ("Tab module" /
|
|
//! "Command module") so the resulting compile errors point at the right
|
|
//! framework. Error messages include the offending decl name and a
|
|
//! verbatim `expected_signature` so the developer can copy-paste the fix.
|
|
|
|
const std = @import("std");
|
|
|
|
/// Assert a decl exists on `Container` with the exact expected type.
|
|
/// `kind` is a short prefix shown in error messages — typically
|
|
/// `"Tab module"` or `"Command module"`.
|
|
pub fn expectDeclWithType(
|
|
comptime kind: []const u8,
|
|
comptime mod_name: []const u8,
|
|
comptime Container: type,
|
|
comptime decl_name: []const u8,
|
|
comptime ExpectedT: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(Container, decl_name)) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(Container, decl_name));
|
|
if (ActualT != ExpectedT) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` has wrong type.\n Expected: " ++ @typeName(ExpectedT) ++
|
|
"\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Expected signature:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Assert a function decl exists on `Container` with the exact fn-pointer
|
|
/// type. Use this for non-fallible functions (`fn (...) T`) or for fns
|
|
/// that explicitly use `anyerror`. For fns whose error set is inferred
|
|
/// (the common `pub fn foo() !void` shape), use `expectFnInferredError`.
|
|
pub fn expectFn(
|
|
comptime kind: []const u8,
|
|
comptime mod_name: []const u8,
|
|
comptime Container: type,
|
|
comptime decl_name: []const u8,
|
|
comptime ExpectedT: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(Container, decl_name)) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(Container, decl_name));
|
|
if (ActualT != ExpectedT) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` has wrong signature.\n Expected: " ++ @typeName(ExpectedT) ++
|
|
"\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Assert a fallible function decl exists on `Container` whose
|
|
/// parameter types match `expected_params` and whose return is an
|
|
/// error union ending in `ExpectedReturn`. The error set itself is
|
|
/// NOT checked — Zig infers a per-fn empty error set for `pub fn foo()
|
|
/// !void` whose name varies per call site, so exact-equality fails.
|
|
/// This loosens equality to "params match, return is `<some_err>!Return`".
|
|
pub fn expectFnInferredError(
|
|
comptime kind: []const u8,
|
|
comptime mod_name: []const u8,
|
|
comptime Container: type,
|
|
comptime decl_name: []const u8,
|
|
comptime expected_params: []const type,
|
|
comptime ExpectedReturn: type,
|
|
comptime expected_signature: []const u8,
|
|
) void {
|
|
comptime {
|
|
if (!@hasDecl(Container, decl_name)) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "` is missing `" ++ decl_name ++
|
|
"`. Expected:\n " ++ expected_signature);
|
|
}
|
|
const ActualT = @TypeOf(@field(Container, decl_name));
|
|
const info = @typeInfo(ActualT);
|
|
if (info != .@"fn") {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` must be a function, got " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
const fn_info = info.@"fn";
|
|
|
|
// Verify params count + types.
|
|
if (fn_info.params.len != expected_params.len) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` has wrong arity.\n Got: " ++ @typeName(ActualT) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
for (expected_params, 0..) |Expected, i| {
|
|
const ActualParam = fn_info.params[i].type orelse {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
|
|
" is `anytype`, but the contract requires `" ++ @typeName(Expected) ++ "`.\n" ++
|
|
" Full expected declaration:\n " ++ expected_signature);
|
|
};
|
|
if (ActualParam != Expected) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` parameter " ++ std.fmt.comptimePrint("{d}", .{i}) ++
|
|
" has wrong type.\n Expected: " ++ @typeName(Expected) ++
|
|
"\n Got: " ++ @typeName(ActualParam) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
|
|
// Verify return is an error union ending in ExpectedReturn.
|
|
const ActualReturn = fn_info.return_type orelse {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` has no return type.\n Full expected declaration:\n " ++ expected_signature);
|
|
};
|
|
const ret_info = @typeInfo(ActualReturn);
|
|
if (ret_info != .error_union) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` must return an error union (`!" ++ @typeName(ExpectedReturn) ++
|
|
"`), got " ++ @typeName(ActualReturn) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
if (ret_info.error_union.payload != ExpectedReturn) {
|
|
@compileError(kind ++ " `" ++ mod_name ++ "`: `" ++ decl_name ++
|
|
"` returns wrong payload type.\n Expected: !" ++ @typeName(ExpectedReturn) ++
|
|
"\n Got: " ++ @typeName(ActualReturn) ++
|
|
"\n Full expected declaration:\n " ++ expected_signature);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────
|
|
//
|
|
// The validators themselves are comptime-only — runtime tests can
|
|
// only exercise the happy path (a decl with the right shape passes
|
|
// silently). The error paths are covered by the fact that every
|
|
// existing tab module compiles, so any breakage to these helpers
|
|
// surfaces immediately at `zig build`.
|
|
|
|
const testing = std.testing;
|
|
|
|
test "expectDeclWithType: matching type passes" {
|
|
const M = struct {
|
|
pub const label: []const u8 = "ok";
|
|
};
|
|
comptime expectDeclWithType("Test", "M", M, "label", []const u8, "pub const label: []const u8 = \"...\";");
|
|
}
|
|
|
|
test "expectFn: matching fn signature passes" {
|
|
const M = struct {
|
|
pub fn handler(_: u32) void {}
|
|
};
|
|
comptime expectFn("Test", "M", M, "handler", fn (u32) void, "pub fn handler(x: u32) void");
|
|
}
|
|
|
|
test "expectFnInferredError: !void function with correct params passes" {
|
|
const M = struct {
|
|
pub fn doit(_: u32, _: []const u8) !void {}
|
|
};
|
|
comptime expectFnInferredError(
|
|
"Test",
|
|
"M",
|
|
M,
|
|
"doit",
|
|
&.{ u32, []const u8 },
|
|
void,
|
|
"pub fn doit(x: u32, s: []const u8) !void",
|
|
);
|
|
}
|
|
|
|
test "expectFnInferredError: !T payload check works for non-void" {
|
|
const M = struct {
|
|
pub fn doit(_: u32) !u64 {
|
|
return 0;
|
|
}
|
|
};
|
|
comptime expectFnInferredError(
|
|
"Test",
|
|
"M",
|
|
M,
|
|
"doit",
|
|
&.{u32},
|
|
u64,
|
|
"pub fn doit(x: u32) !u64",
|
|
);
|
|
}
|