Template
feat(kdl): add DSL schema validation
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
/*
|
||||
* schema.c - stupidtools DSL schema validation (todo 9).
|
||||
*
|
||||
* Validates a parsed KDL document (src/kdl/ast.h) against the stupidtools
|
||||
* DSL. The grammar is pinned in schema.h - the contract for todos 10/16/23.
|
||||
* This module only READS the AST; it never modifies or frees it.
|
||||
*
|
||||
* Type checks go through the value model (src/kdl/value.h): names, files,
|
||||
* version strings and check arguments must be non-empty unannotated
|
||||
* strings; an option's `default` must be an unannotated boolean. Value-
|
||||
* model failures (bad escapes, overflow) are re-owned as schema errors.
|
||||
*
|
||||
* ERRORS AND SPANS
|
||||
* ----------------
|
||||
* Every failure returns an owned st_error of category ST_ERR_KDL_SCHEMA
|
||||
* whose span is heap-allocated IN THE SAME BLOCK as the error struct
|
||||
* (the parser.c pattern), so err->span stays valid until st_error_free()
|
||||
* and is never dangling. The span is borrowed-by-value from the AST's
|
||||
* token refs, whose `file` pointer in turn borrows the caller's source
|
||||
* buffer - the same lifetime contract as the parser. The single span-less
|
||||
* error is "missing project" on an empty document: there is no source
|
||||
* position (or file name) to point at.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "kdl/schema.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/value.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- owned errors ----------------------------------------------------- */
|
||||
|
||||
/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same
|
||||
* allocation as the error struct (aligned right after it). Returns NULL
|
||||
* only on allocation failure. */
|
||||
static struct st_error *
|
||||
err_at_owned(struct st_span sp, const char *msg)
|
||||
{
|
||||
const size_t align = _Alignof(struct st_span);
|
||||
const size_t esize =
|
||||
(sizeof(struct st_error) + align - 1) & ~(align - 1);
|
||||
struct st_error *e;
|
||||
struct st_span *spc;
|
||||
size_t mlen;
|
||||
|
||||
if (msg == NULL) {
|
||||
msg = "";
|
||||
}
|
||||
e = malloc(esize + sizeof(struct st_span));
|
||||
if (e == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
mlen = strlen(msg);
|
||||
e->message = malloc(mlen + 1);
|
||||
if (e->message == NULL) {
|
||||
free(e);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(e->message, msg, mlen + 1);
|
||||
e->category = ST_ERR_KDL_SCHEMA;
|
||||
spc = (struct st_span *)((unsigned char *)e + esize);
|
||||
*spc = sp;
|
||||
e->span = spc;
|
||||
return e;
|
||||
}
|
||||
|
||||
/* Re-own a value-model (ST_ERR_KDL_PARSE) error as a schema error: copy
|
||||
* the message and the span VALUE into a fresh block. The caller frees the
|
||||
* original error. */
|
||||
static struct st_error *
|
||||
schema_err_from(const struct st_error *e)
|
||||
{
|
||||
struct st_span sp;
|
||||
|
||||
if (e->span != NULL) {
|
||||
sp = *e->span;
|
||||
} else {
|
||||
sp = (struct st_span){ NULL, 0, 0 };
|
||||
}
|
||||
return err_at_owned(sp, e->message != NULL ? e->message : "");
|
||||
}
|
||||
|
||||
/* Interpret a token ref through the value model, converting any failure
|
||||
* into an owned schema error (the original is freed here). On success
|
||||
* `*out` holds the interpreted value (caller frees via
|
||||
* st_kdl_value_free). */
|
||||
static struct st_error *
|
||||
value_interpret(const struct st_kdl_token_ref *tok,
|
||||
const struct st_kdl_token_ref *ann,
|
||||
struct st_kdl_value *out)
|
||||
{
|
||||
struct st_error *e = st_kdl_value_from_token_annotated(tok, ann, out);
|
||||
|
||||
if (e != NULL) {
|
||||
struct st_error *r = schema_err_from(e);
|
||||
|
||||
st_error_free(e);
|
||||
return r;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- naming helpers --------------------------------------------------- */
|
||||
|
||||
/* Copy a node name / property key into a fixed buffer for MESSAGE text,
|
||||
* truncating to fit. Identifiers are copied raw (the common case, no
|
||||
* allocation); other string forms are interpreted via the value model,
|
||||
* falling back to "?" when that fails (message-only, never fatal). */
|
||||
static void
|
||||
name_into(const struct st_kdl_token_ref *name, char *buf, size_t cap)
|
||||
{
|
||||
if (cap == 0) {
|
||||
return;
|
||||
}
|
||||
if (name->kind == ST_TOK_IDENT) {
|
||||
size_t n = name->len < cap - 1 ? name->len : cap - 1;
|
||||
|
||||
memcpy(buf, name->text, n);
|
||||
buf[n] = '\0';
|
||||
return;
|
||||
}
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e = st_kdl_value_from_token(name, &v);
|
||||
|
||||
if (e == NULL && v.kind == ST_KDL_VAL_STRING) {
|
||||
snprintf(buf, cap, "%s", v.as.str);
|
||||
} else {
|
||||
snprintf(buf, cap, "?");
|
||||
}
|
||||
st_error_free(e);
|
||||
st_kdl_value_free(&v);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compare a node name against a literal, accepting every string form
|
||||
* (identifiers compared directly; other forms via the value model).
|
||||
* Sets *out and returns NULL on success, or an owned schema error when
|
||||
* the name is a string the value model cannot interpret. */
|
||||
static struct st_error *
|
||||
name_equals(const struct st_kdl_token_ref *name, const char *s, bool *out)
|
||||
{
|
||||
if (name->kind == ST_TOK_IDENT) {
|
||||
*out = name->len == strlen(s) &&
|
||||
memcmp(name->text, s, name->len) == 0;
|
||||
return NULL;
|
||||
}
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e = st_kdl_value_from_token(name, &v);
|
||||
|
||||
if (e != NULL) {
|
||||
struct st_error *r = schema_err_from(e);
|
||||
|
||||
st_error_free(e);
|
||||
*out = false;
|
||||
return r;
|
||||
}
|
||||
*out = v.kind == ST_KDL_VAL_STRING && strcmp(v.as.str, s) == 0;
|
||||
st_kdl_value_free(&v);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- argument helpers ------------------------------------------------- */
|
||||
|
||||
/* Interpret `tok` (with optional annotation) as the non-empty unannotated
|
||||
* string a DSL slot requires; on success *out owns the string (caller
|
||||
* frees). `ctx` names the slot in messages, e.g. "target name". */
|
||||
static struct st_error *
|
||||
require_string_arg(const struct st_kdl_token_ref *tok,
|
||||
const struct st_kdl_token_ref *ann,
|
||||
const char *ctx, char **out)
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e;
|
||||
char msg[192];
|
||||
|
||||
*out = NULL;
|
||||
e = value_interpret(tok, ann, &v);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
if (v.annotation != NULL) {
|
||||
snprintf(msg, sizeof msg, "%s must not carry a type annotation",
|
||||
ctx);
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
if (v.kind != ST_KDL_VAL_STRING) {
|
||||
snprintf(msg, sizeof msg, "%s must be a non-empty string (got %s)",
|
||||
ctx, st_kdl_value_kind_name(v.kind));
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
if (v.as.str[0] == '\0') {
|
||||
snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx);
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
*out = v.as.str;
|
||||
v.as.str = NULL;
|
||||
st_kdl_value_free(&v); /* frees the annotation only */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The shared name shape of target/feature/option: exactly one non-empty
|
||||
* string argument. `ctx` is the node kind ("target"). On success *out
|
||||
* owns the name (caller frees). */
|
||||
static struct st_error *
|
||||
one_name_arg(const struct st_kdl_node *n, const char *ctx, char **out)
|
||||
{
|
||||
char msg[192];
|
||||
char cctx[96];
|
||||
|
||||
*out = NULL;
|
||||
if (n->args == NULL) {
|
||||
snprintf(msg, sizeof msg, "%s requires a name argument", ctx);
|
||||
return err_at_owned(n->name.span, msg);
|
||||
}
|
||||
if (n->args->next != NULL) {
|
||||
char nm[65];
|
||||
|
||||
name_into(&n->args->value, nm, sizeof nm);
|
||||
snprintf(msg, sizeof msg, "%s '%s' takes exactly one argument",
|
||||
ctx, nm);
|
||||
return err_at_owned(n->args->next->value.span, msg);
|
||||
}
|
||||
snprintf(cctx, sizeof cctx, "%s name", ctx);
|
||||
return require_string_arg(&n->args->value, n->args->annotation, cctx,
|
||||
out);
|
||||
}
|
||||
|
||||
/* ---- per-node validators ---------------------------------------------- */
|
||||
|
||||
/* project "name" version "semver": the name is the first positional
|
||||
* argument; the version string follows the LITERAL `version` keyword.
|
||||
* No properties, no children, exactly three arguments. */
|
||||
static struct st_error *
|
||||
validate_project(const struct st_kdl_node *n)
|
||||
{
|
||||
struct st_kdl_arg *a = n->args;
|
||||
struct st_kdl_arg *ver_kw;
|
||||
char msg[192];
|
||||
char name[65];
|
||||
char ctx[96];
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
char *ver_str = NULL;
|
||||
|
||||
if (a == NULL) {
|
||||
return err_at_owned(n->name.span,
|
||||
"project requires a name argument");
|
||||
}
|
||||
e = require_string_arg(&a->value, a->annotation, "project name",
|
||||
&name_str);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
name_into(&a->value, name, sizeof name);
|
||||
|
||||
a = a->next;
|
||||
if (a == NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"project '%s' requires a version (expected: project \"name\" "
|
||||
"version \"semver\")", name);
|
||||
e = err_at_owned(n->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (a->annotation != NULL ||
|
||||
!(a->value.kind == ST_TOK_IDENT && a->value.len == 7 &&
|
||||
memcmp(a->value.text, "version", 7) == 0)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"project '%s': expected the keyword 'version' as the second "
|
||||
"argument", name);
|
||||
e = err_at_owned(a->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
ver_kw = a;
|
||||
a = a->next;
|
||||
if (a == NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"project '%s': 'version' requires the version string argument",
|
||||
name);
|
||||
e = err_at_owned(ver_kw->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
snprintf(ctx, sizeof ctx, "project '%s' version", name);
|
||||
e = require_string_arg(&a->value, a->annotation, ctx, &ver_str);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (a->next != NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"project '%s' takes exactly three arguments: the name, the "
|
||||
"'version' keyword, and the version string", name);
|
||||
e = err_at_owned(a->next->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (n->props != NULL) {
|
||||
char key[65];
|
||||
|
||||
name_into(&n->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg, "project '%s' has unexpected property "
|
||||
"'%s'", name, key);
|
||||
e = err_at_owned(n->props->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (n->children != NULL) {
|
||||
snprintf(msg, sizeof msg, "project '%s' takes no children", name);
|
||||
e = err_at_owned(n->children->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
e = NULL;
|
||||
done:
|
||||
free(name_str);
|
||||
free(ver_str);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* One `src "<file>"` or `feature "<name>"` child of a target: exactly one
|
||||
* non-empty string argument, no properties, no children. */
|
||||
static struct st_error *
|
||||
validate_target_child(const struct st_kdl_node *c, const char *ctx,
|
||||
bool is_src)
|
||||
{
|
||||
const char *kind = is_src ? "src" : "feature";
|
||||
const char *slot = is_src ? "file" : "name";
|
||||
char cctx[192];
|
||||
char msg[192];
|
||||
|
||||
if (c->args == NULL) {
|
||||
snprintf(msg, sizeof msg, "%s '%s' requires a %s argument", ctx,
|
||||
kind, slot);
|
||||
return err_at_owned(c->name.span, msg);
|
||||
}
|
||||
if (c->args->next != NULL) {
|
||||
snprintf(msg, sizeof msg, "%s '%s' takes exactly one argument",
|
||||
ctx, kind);
|
||||
return err_at_owned(c->args->next->value.span, msg);
|
||||
}
|
||||
if (c->props != NULL) {
|
||||
snprintf(msg, sizeof msg, "%s '%s' takes no properties", ctx, kind);
|
||||
return err_at_owned(c->props->key.span, msg);
|
||||
}
|
||||
if (c->children != NULL) {
|
||||
snprintf(msg, sizeof msg, "%s '%s' takes no children", ctx, kind);
|
||||
return err_at_owned(c->children->name.span, msg);
|
||||
}
|
||||
{
|
||||
char *tmp = NULL;
|
||||
struct st_error *e;
|
||||
|
||||
snprintf(cctx, sizeof cctx, "%s '%s' %s", ctx, kind, slot);
|
||||
e = require_string_arg(&c->args->value, c->args->annotation, cctx,
|
||||
&tmp);
|
||||
free(tmp);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
/* target "name" { src ...; feature ...; }: the name shape plus a children
|
||||
* block whose nodes are only `src` (a file) or `feature` (a reference to
|
||||
* a top-level feature by name). Presence-checking the reference against
|
||||
* the document is OPTIONAL for this todo - only the shape is enforced. */
|
||||
static struct st_error *
|
||||
validate_target(const struct st_kdl_node *n)
|
||||
{
|
||||
char msg[256];
|
||||
char name[65];
|
||||
char ctx[96];
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
struct st_kdl_node *c;
|
||||
|
||||
e = one_name_arg(n, "target", &name_str);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
name_into(&n->args->value, name, sizeof name);
|
||||
|
||||
if (n->props != NULL) {
|
||||
char key[65];
|
||||
|
||||
name_into(&n->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg, "target '%s' has unexpected property "
|
||||
"'%s'", name, key);
|
||||
e = err_at_owned(n->props->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
|
||||
snprintf(ctx, sizeof ctx, "target '%s'", name);
|
||||
for (c = n->children; c != NULL; c = c->next) {
|
||||
bool is_src = false;
|
||||
bool is_feat = false;
|
||||
|
||||
e = name_equals(&c->name, "src", &is_src);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
e = name_equals(&c->name, "feature", &is_feat);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (is_src || is_feat) {
|
||||
e = validate_target_child(c, ctx, is_src);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
{
|
||||
char cname[65];
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(msg, sizeof msg,
|
||||
"%s has unknown child node '%s' (expected 'src' or "
|
||||
"'feature')", ctx, cname);
|
||||
e = err_at_owned(c->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
e = NULL;
|
||||
done:
|
||||
free(name_str);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* feature "name" { <checks> }: the name shape (no properties) plus a
|
||||
* children block of CHECK nodes, validated STRUCTURALLY ONLY. The 8
|
||||
* check kinds and their exact argument/property shapes are todo 10's job;
|
||||
* until its registry lands, every child is accepted provided it has at
|
||||
* least one non-empty string argument and no children (its properties
|
||||
* are deliberately left for todo 10 to validate). */
|
||||
static struct st_error *
|
||||
validate_feature(const struct st_kdl_node *n)
|
||||
{
|
||||
char msg[192];
|
||||
char name[65];
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
struct st_kdl_node *c;
|
||||
|
||||
e = one_name_arg(n, "feature", &name_str);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
name_into(&n->args->value, name, sizeof name);
|
||||
|
||||
if (n->props != NULL) {
|
||||
char key[65];
|
||||
|
||||
name_into(&n->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg, "feature '%s' has unexpected property "
|
||||
"'%s'", name, key);
|
||||
e = err_at_owned(n->props->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
|
||||
for (c = n->children; c != NULL; c = c->next) {
|
||||
/*
|
||||
* HOOK (todo 10): dispatch on the check node NAME through the
|
||||
* feature-check registry (src/detect/check_registry.h) to
|
||||
* validate the 8 kinds - header, function, library, type,
|
||||
* sizeof, program, compiler_flag, pkg_config - and their
|
||||
* argument/property shapes. Until then, structural checks only:
|
||||
*/
|
||||
if (c->args == NULL) {
|
||||
char cname[65];
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' requires an argument", name,
|
||||
cname);
|
||||
e = err_at_owned(c->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
{
|
||||
char cname[65];
|
||||
char cctx[192];
|
||||
char *tmp = NULL;
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(cctx, sizeof cctx, "feature '%s': check '%s' argument",
|
||||
name, cname);
|
||||
e = require_string_arg(&c->args->value, c->args->annotation,
|
||||
cctx, &tmp);
|
||||
free(tmp);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
if (c->children != NULL) {
|
||||
char cname[65];
|
||||
|
||||
name_into(&c->name, cname, sizeof cname);
|
||||
snprintf(msg, sizeof msg,
|
||||
"feature '%s': check '%s' takes no children", name, cname);
|
||||
e = err_at_owned(c->children->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
e = NULL;
|
||||
done:
|
||||
free(name_str);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* option "name" (default=<bool>)?: the name shape, no children, and at
|
||||
* most one `default` property whose value must be an unannotated boolean
|
||||
* (#true/#false) once interpreted through the value model. */
|
||||
static struct st_error *
|
||||
validate_option(const struct st_kdl_node *n)
|
||||
{
|
||||
char msg[256];
|
||||
char name[65];
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
struct st_kdl_prop *p;
|
||||
bool seen_default = false;
|
||||
|
||||
e = one_name_arg(n, "option", &name_str);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
name_into(&n->args->value, name, sizeof name);
|
||||
|
||||
if (n->children != NULL) {
|
||||
snprintf(msg, sizeof msg, "option '%s' takes no children", name);
|
||||
e = err_at_owned(n->children->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
|
||||
for (p = n->props; p != NULL; p = p->next) {
|
||||
bool is_def = false;
|
||||
|
||||
e = name_equals(&p->key, "default", &is_def);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (!is_def) {
|
||||
char key[65];
|
||||
|
||||
name_into(&p->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg,
|
||||
"option '%s' has unexpected property '%s' (only 'default' "
|
||||
"is allowed)", name, key);
|
||||
e = err_at_owned(p->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (seen_default) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"option '%s' has a duplicate 'default' property", name);
|
||||
e = err_at_owned(p->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
seen_default = true;
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
|
||||
e = value_interpret(&p->value, p->annotation, &v);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (v.annotation != NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"option '%s': 'default' must not carry a type "
|
||||
"annotation", name);
|
||||
e = err_at_owned(p->value.span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
goto done;
|
||||
}
|
||||
if (v.kind != ST_KDL_VAL_BOOL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"option '%s': 'default' must be a boolean (got %s)",
|
||||
name, st_kdl_value_kind_name(v.kind));
|
||||
e = err_at_owned(p->value.span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
goto done;
|
||||
}
|
||||
st_kdl_value_free(&v);
|
||||
}
|
||||
}
|
||||
e = NULL;
|
||||
done:
|
||||
free(name_str);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* ---- top level -------------------------------------------------------- */
|
||||
|
||||
/* Classify a top-level node name. */
|
||||
enum top_kind {
|
||||
TOP_PROJECT = 0,
|
||||
TOP_TARGET,
|
||||
TOP_FEATURE,
|
||||
TOP_OPTION,
|
||||
TOP_UNKNOWN,
|
||||
};
|
||||
|
||||
static struct st_error *
|
||||
classify_top(const struct st_kdl_node *n, enum top_kind *out)
|
||||
{
|
||||
static const char *const names[] = { "project", "target", "feature",
|
||||
"option" };
|
||||
static const enum top_kind kinds[] = { TOP_PROJECT, TOP_TARGET,
|
||||
TOP_FEATURE, TOP_OPTION };
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
||||
bool eq = false;
|
||||
struct st_error *e = name_equals(&n->name, names[i], &eq);
|
||||
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
if (eq) {
|
||||
*out = kinds[i];
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
*out = TOP_UNKNOWN;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- public API ------------------------------------------------------- */
|
||||
|
||||
struct st_error *
|
||||
st_kdl_validate(const struct st_kdl_document *doc)
|
||||
{
|
||||
struct st_kdl_node *n;
|
||||
bool saw_goal = false; /* saw at least one target or feature */
|
||||
struct st_error *e;
|
||||
|
||||
if (doc == NULL || doc->nodes == NULL) {
|
||||
/* No source position (or file name) exists to point at. */
|
||||
return st_error_kdl_schema("missing required 'project' node");
|
||||
}
|
||||
|
||||
n = doc->nodes;
|
||||
{
|
||||
enum top_kind kind;
|
||||
|
||||
e = classify_top(n, &kind);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
if (kind != TOP_PROJECT) {
|
||||
char nm[65];
|
||||
char msg[192];
|
||||
|
||||
name_into(&n->name, nm, sizeof nm);
|
||||
snprintf(msg, sizeof msg,
|
||||
"expected 'project' as the first top-level node, got '%s'",
|
||||
nm);
|
||||
return err_at_owned(n->name.span, msg);
|
||||
}
|
||||
e = validate_project(n);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
for (n = n->next; n != NULL; n = n->next) {
|
||||
enum top_kind kind;
|
||||
|
||||
e = classify_top(n, &kind);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
switch (kind) {
|
||||
case TOP_PROJECT:
|
||||
return err_at_owned(n->name.span, "duplicate 'project' node");
|
||||
case TOP_TARGET:
|
||||
e = validate_target(n);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
saw_goal = true;
|
||||
break;
|
||||
case TOP_FEATURE:
|
||||
e = validate_feature(n);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
saw_goal = true;
|
||||
break;
|
||||
case TOP_OPTION:
|
||||
e = validate_option(n);
|
||||
if (e != NULL) {
|
||||
return e;
|
||||
}
|
||||
break;
|
||||
case TOP_UNKNOWN: {
|
||||
char nm[65];
|
||||
char msg[192];
|
||||
|
||||
name_into(&n->name, nm, sizeof nm);
|
||||
snprintf(msg, sizeof msg,
|
||||
"unknown top-level node '%s' (expected 'target', "
|
||||
"'feature', or 'option')", nm);
|
||||
return err_at_owned(n->name.span, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!saw_goal) {
|
||||
return err_at_owned(doc->nodes->name.span,
|
||||
"missing 'target' or 'feature' node after 'project'");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* schema.h - stupidtools DSL schema validation (todo 9).
|
||||
*
|
||||
* st_kdl_validate() checks a parsed document (src/kdl/ast.h) against the
|
||||
* stupidtools build-file DSL - a KDL subset. THE GRAMMAR PINNED HERE IS THE
|
||||
* CONTRACT consumed by todo 10 (feature-check registry), todo 16 (configure
|
||||
* generation) and todo 23 (self-host). Change it only deliberately:
|
||||
*
|
||||
* document := project (target | feature | option)* project FIRST
|
||||
* project := 'project' <name> version <semver>
|
||||
* target := 'target' <name> '{' (src | feature)* '}'
|
||||
* src := 'src' <file>
|
||||
* featref := 'feature' <name> (name of a top-level feature)
|
||||
* feature := 'feature' <name> '{' check* '}'
|
||||
* check := <name> <arg> props* STRUCTURAL ONLY until todo 10
|
||||
* option := 'option' <name> (default '=' <bool>)?
|
||||
*
|
||||
* <name>/<file>/<arg>/<semver> are KDL strings (identifier or any quoted/
|
||||
* raw/multi-line form), all required to be NON-EMPTY and unannotated;
|
||||
* <bool> is an unannotated #true/#false. At least one target or feature
|
||||
* must follow the project node.
|
||||
*
|
||||
* PINNED DECISIONS (deliberate; the plan's shorthand left these open):
|
||||
* - project version syntax: `project "p" version "1.0.0"` - the version
|
||||
* is the argument AFTER the literal `version` keyword (a bare
|
||||
* identifier). The property form `version="..."` is NOT accepted.
|
||||
* - option default: an optional `default` PROPERTY (`option "x"
|
||||
* default=#true`), not a child node.
|
||||
* - feature children are validated structurally only: at least one
|
||||
* non-empty string argument, no children, arbitrary properties. The
|
||||
* 8 check kinds (header/function/library/type/sizeof/program/
|
||||
* compiler_flag/pkg_config) and their exact argument/property shapes
|
||||
* are todo 10's job - see the marked HOOK in schema.c.
|
||||
* - the semver FORMAT is not checked (any non-empty string is accepted).
|
||||
* - duplicate NAMES among target/feature/option are not checked
|
||||
* (deferred to the generation todos; duplicate project IS rejected).
|
||||
*
|
||||
* ERRORS
|
||||
* ------
|
||||
* Returns NULL on success, or an owned st_error (ST_ERR_KDL_SCHEMA) whose
|
||||
* span is heap-allocated IN THE SAME BLOCK as the error (the parser.c
|
||||
* pattern), so err->span stays valid until st_error_free(err). The one
|
||||
* span-less case: an empty (or NULL) document, where no source position or
|
||||
* file name exists to point at.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_KDL_SCHEMA_H
|
||||
#define ST_KDL_SCHEMA_H
|
||||
|
||||
#include "ast.h" /* struct st_kdl_document */
|
||||
|
||||
struct st_error;
|
||||
|
||||
/* Validate a parsed document against the stupidtools DSL (grammar above).
|
||||
* Returns NULL on success; on failure returns an owned ST_ERR_KDL_SCHEMA
|
||||
* error naming the offending node. The document is only read - never
|
||||
* modified and never freed. */
|
||||
struct st_error *st_kdl_validate(const struct st_kdl_document *doc);
|
||||
|
||||
#endif /* ST_KDL_SCHEMA_H */
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
/* stupid.kdl - the canonical stupidtools build file (todo 9 fixture).
|
||||
|
||||
Pins the stupidtools DSL grammar (enforced by src/kdl/schema.h):
|
||||
|
||||
project "name" version "semver" required, MUST be the first node
|
||||
target "name" { src "file" ... ; feature "name" ... }
|
||||
feature "name" { <checks> } checks: structural until todo 10
|
||||
option "name" default=#bool default is an optional bool property
|
||||
|
||||
This is the VALID fixture for tests/unit/test_schema.c and is reused by
|
||||
todo 16 (configure generation) and todo 23 (self-host), so it is
|
||||
representative of a real build file, not a minimal toy. */
|
||||
|
||||
// The project node: the name is the first positional argument; the
|
||||
// version string follows the literal `version` keyword.
|
||||
project "stupidtools" version "1.0.0"
|
||||
|
||||
// Features bundle the checks a target depends on. `header` and `library`
|
||||
// are two of the 8 check kinds (todo 10 defines them all); here they are
|
||||
// structural placeholders.
|
||||
feature "pthread" {
|
||||
header "pthread.h"
|
||||
library "pthread"
|
||||
}
|
||||
|
||||
feature "math" {
|
||||
library "m"
|
||||
}
|
||||
|
||||
// A target: the sources that make up one artifact, plus the features it
|
||||
// needs at build/link time.
|
||||
target "default" {
|
||||
src "src/main.c"
|
||||
src "src/cli.c"
|
||||
src "src/kdl/lexer.c"
|
||||
src "src/kdl/parser.c"
|
||||
feature "pthread"
|
||||
feature "math"
|
||||
}
|
||||
|
||||
// An option maps to --enable-debug / --disable-debug; `default` is an
|
||||
// optional boolean property (unannotated #true/#false only).
|
||||
option "debug" default=#false
|
||||
@@ -0,0 +1,529 @@
|
||||
/* LINK: ../../src/kdl/schema.c ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
|
||||
/* tests/unit/test_schema.c
|
||||
*
|
||||
* Unit tests for the stupidtools DSL schema validator (todo 9).
|
||||
*
|
||||
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
|
||||
* the extra .c sources to compile into this test binary (paths relative
|
||||
* to tests/unit/, space-separated). schema.c depends on value.c (typed
|
||||
* value interpretation) and error.c (typed errors); parser.c + lexer.c
|
||||
* are linked so documents can be built end-to-end from source text, and
|
||||
* span.c via error.c's st_span_print.
|
||||
*
|
||||
* The grammar under test is pinned in src/kdl/schema.h. Assertions check
|
||||
* REAL error properties: category (always ST_ERR_KDL_SCHEMA), message
|
||||
* text (must name the offending node), and exact line/col spans — never
|
||||
* just "returns an error".
|
||||
*/
|
||||
#include "munit.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/schema.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- fixture loading -------------------------------------------------- */
|
||||
|
||||
/* Locate tests/fixtures/stupid.kdl. The harness runs the test binary with
|
||||
* cwd = repo top (make check) or tests/unit (manual run); probe both. */
|
||||
static const char *
|
||||
fixture_path(void)
|
||||
{
|
||||
static const char *const candidates[] = {
|
||||
"tests/fixtures/stupid.kdl",
|
||||
"../fixtures/stupid.kdl",
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
FILE *f = fopen(candidates[i], "rb");
|
||||
if (f != NULL) {
|
||||
fclose(f);
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Slurp the fixture into a NUL-terminated buffer (the lexer requires NUL
|
||||
* termination). Returns NULL on any I/O failure. Caller frees. */
|
||||
static char *
|
||||
slurp_fixture(const char *path)
|
||||
{
|
||||
FILE *f;
|
||||
long n;
|
||||
char *buf;
|
||||
|
||||
if (path == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
f = fopen(path, "rb");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
n = ftell(f);
|
||||
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
buf = munit_malloc((size_t)n + 1);
|
||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
|
||||
free(buf);
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
buf[n] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ---- helpers ---------------------------------------------------------- */
|
||||
|
||||
/* Parse `src` (asserting parse success) and validate; assert the result
|
||||
* is a ST_ERR_KDL_SCHEMA error whose message contains `needle` and whose
|
||||
* span is exactly (line, col). */
|
||||
static void
|
||||
assert_schema_error(const char *src, const char *needle, size_t line,
|
||||
size_t col)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = st_kdl_parse(src, "t.kdl", &err);
|
||||
|
||||
munit_assert_null(err);
|
||||
munit_assert_not_null(doc);
|
||||
err = st_kdl_validate(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
munit_assert_not_null(err->span);
|
||||
munit_assert_size(err->span->line, ==, line);
|
||||
munit_assert_size(err->span->col, ==, col);
|
||||
munit_assert_true(strstr(st_error_message(err), needle) != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
}
|
||||
|
||||
/* Parse `src` (asserting parse success) and validate; assert success. */
|
||||
static void
|
||||
assert_valid(const char *src)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = st_kdl_parse(src, "t.kdl", &err);
|
||||
|
||||
munit_assert_null(err);
|
||||
munit_assert_not_null(doc);
|
||||
err = st_kdl_validate(doc);
|
||||
munit_assert_null(err);
|
||||
st_kdl_document_free(doc);
|
||||
}
|
||||
|
||||
/* ---- tests ------------------------------------------------------------ */
|
||||
|
||||
/* (a) tests/fixtures/stupid.kdl validates clean and carries the expected
|
||||
* canonical shapes: project first (name + version keyword + semver), a
|
||||
* target with src/feature children, features with header/library check
|
||||
* placeholders, and an option with a bool default. */
|
||||
static MunitResult
|
||||
test_valid_fixture(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
const char *path = fixture_path();
|
||||
char *src;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
struct st_kdl_node *n;
|
||||
size_t top_count = 0;
|
||||
size_t target_children = 0;
|
||||
|
||||
munit_assert_not_null(path);
|
||||
src = slurp_fixture(path);
|
||||
munit_assert_not_null(src);
|
||||
|
||||
doc = st_kdl_parse(src, path, &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
|
||||
/* validates clean (the core acceptance of this todo) */
|
||||
munit_assert_null(st_kdl_validate(doc));
|
||||
|
||||
/* five top-level nodes: project, feature, feature, target, option */
|
||||
for (n = doc->nodes; n != NULL; n = n->next) {
|
||||
top_count++;
|
||||
}
|
||||
munit_assert_size(top_count, ==, 5);
|
||||
|
||||
/* project spans (hand-verified against cat -n of the fixture) */
|
||||
n = doc->nodes;
|
||||
munit_assert_size(n->name.span.line, ==, 16);
|
||||
munit_assert_size(n->name.span.col, ==, 1);
|
||||
munit_assert_not_null(n->args);
|
||||
munit_assert_size(n->args->value.span.col, ==, 9); /* "stupidtools" */
|
||||
munit_assert_size(n->args->next->value.span.col, ==, 23); /* version kw */
|
||||
munit_assert_size(n->args->next->next->value.span.col, ==, 31); /* "1.0.0" */
|
||||
|
||||
/* target "default" has 6 children: 4 src + 2 feature refs */
|
||||
for (n = doc->nodes; n != NULL && top_count-- > 0; n = n->next) {
|
||||
if (n->name.kind == ST_TOK_IDENT && n->name.len == 6 &&
|
||||
memcmp(n->name.text, "target", 6) == 0) {
|
||||
struct st_kdl_node *c;
|
||||
for (c = n->children; c != NULL; c = c->next) {
|
||||
target_children++;
|
||||
}
|
||||
}
|
||||
}
|
||||
munit_assert_size(target_children, ==, 6);
|
||||
|
||||
/* option "debug" default=#false: key and value spans */
|
||||
n = doc->nodes->next->next->next->next;
|
||||
munit_assert_size(n->name.span.line, ==, 43);
|
||||
munit_assert_size(n->args->value.span.col, ==, 8); /* "debug" */
|
||||
munit_assert_not_null(n->props);
|
||||
munit_assert_size(n->props->key.span.col, ==, 16); /* default */
|
||||
munit_assert_size(n->props->value.span.col, ==, 24); /* #false */
|
||||
|
||||
st_kdl_document_free(doc);
|
||||
free(src);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (c) missing project: an empty document (and a NULL document) yields a
|
||||
* schema error with NO span (no file name is available for an empty
|
||||
* document). */
|
||||
static MunitResult
|
||||
test_missing_project(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc;
|
||||
|
||||
doc = st_kdl_parse("", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
err = st_kdl_validate(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
munit_assert_null(err->span);
|
||||
munit_assert_true(
|
||||
strstr(st_error_message(err), "missing required 'project'") != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
|
||||
/* comment-only documents are equally empty */
|
||||
doc = st_kdl_parse("// nothing\n", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
err = st_kdl_validate(doc);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_null(err->span);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
|
||||
/* a NULL document is the same failure */
|
||||
err = st_kdl_validate(NULL);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
st_error_free(err);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (c/d) project must be FIRST: a document starting with a non-project
|
||||
* node errors naming that node with its span. */
|
||||
static MunitResult
|
||||
test_project_not_first(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error("target \"x\" { src \"a.c\" }\n"
|
||||
"project \"p\" version \"1.0\"",
|
||||
"'target'", 1, 1);
|
||||
assert_schema_error("feature \"f\" { header \"h.h\" }\n"
|
||||
"project \"p\" version \"1.0\"",
|
||||
"'feature'", 1, 1);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (b) unknown top-level node names the node + span, in any position after
|
||||
* the project. */
|
||||
static MunitResult
|
||||
test_unknown_top_level(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error("project \"p\" version \"1.0\"\n"
|
||||
"bogus \"x\"\n",
|
||||
"'bogus'", 2, 1);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"wat 1\n",
|
||||
"'wat'", 3, 1);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* duplicate project nodes are rejected with the second project's span. */
|
||||
static MunitResult
|
||||
test_duplicate_project(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error("project \"p\" version \"1.0\"\n"
|
||||
"project \"q\" version \"2.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n",
|
||||
"duplicate 'project'", 2, 1);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (e) missing/extra required arguments on every node shape. */
|
||||
static MunitResult
|
||||
test_missing_args(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
/* project argument shapes */
|
||||
assert_schema_error("project", "requires a name argument", 1, 1);
|
||||
assert_schema_error("project \"p\"", "requires a version", 1, 1);
|
||||
assert_schema_error("project \"p\" version",
|
||||
"requires the version string", 1, 13);
|
||||
assert_schema_error("project \"p\" version \"1.0\" extra",
|
||||
"exactly three arguments", 1, 27);
|
||||
|
||||
/* target/feature/option names */
|
||||
assert_schema_error("project \"p\" version \"1.0\"\ntarget",
|
||||
"target requires a name argument", 2, 1);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\ntarget \"t\" \"u\"",
|
||||
"takes exactly one argument", 2, 12);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\nfeature",
|
||||
"feature requires a name argument", 2, 1);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\noption",
|
||||
"option requires a name argument", 2, 1);
|
||||
|
||||
/* project-only: a build file needs at least one target or feature */
|
||||
assert_schema_error("project \"p\" version \"1.0\"",
|
||||
"missing 'target' or 'feature'", 1, 1);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\noption \"x\"",
|
||||
"missing 'target' or 'feature'", 1, 1);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* wrong argument types are rejected via the value model, naming what was
|
||||
* expected and what was found. */
|
||||
static MunitResult
|
||||
test_wrong_arg_types(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error("project 42 version \"1.0\"",
|
||||
"project name must be a non-empty string (got int)",
|
||||
1, 9);
|
||||
assert_schema_error("project \"p\" 99 \"1.0\"",
|
||||
"expected the keyword 'version'", 1, 13);
|
||||
assert_schema_error("project \"p\" version 42",
|
||||
"version must be a non-empty string (got int)", 1, 21);
|
||||
assert_schema_error("project \"p\" version \"\"",
|
||||
"version must be a non-empty string", 1, 21);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\ntarget 42",
|
||||
"target name must be a non-empty string (got int)",
|
||||
2, 8);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* project extras: properties and children are not part of the pinned
|
||||
* grammar. */
|
||||
static MunitResult
|
||||
test_project_extras(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error("project \"p\" version \"1.0\" foo=1",
|
||||
"unexpected property 'foo'", 1, 27);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\" { target \"t\" { src \"a.c\" } }",
|
||||
"takes no children", 1, 29);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (g) unknown target children name the child + span; known children get
|
||||
* their argument/property/children shapes enforced. */
|
||||
static MunitResult
|
||||
test_target_children(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { cc-flags \"-O2\" }",
|
||||
"'cc-flags'", 2, 14);
|
||||
assert_schema_error("project \"p\" version \"1.0\"\ntarget \"t\" { jobs 8 }",
|
||||
"'jobs'", 2, 14);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { src }",
|
||||
"'src' requires a file argument", 2, 14);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { src 42 }",
|
||||
"'src' file must be a non-empty string (got int)", 2, 18);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" extra }",
|
||||
"'src' takes exactly one argument", 2, 24);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" foo=1 }",
|
||||
"'src' takes no properties", 2, 24);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { src \"a.c\" { x } }",
|
||||
"'src' takes no children", 2, 26);
|
||||
|
||||
/* feature references inside a target */
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { feature }",
|
||||
"'feature' requires a name argument", 2, 14);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { feature 42 }",
|
||||
"'feature' name must be a non-empty string (got int)", 2, 22);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\ntarget \"t\" { feature \"p\" foo=1 }",
|
||||
"'feature' takes no properties", 2, 26);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* feature children: structural validation only (the 8 check kinds are
|
||||
* todo 10). Any check name passes provided it has >= 1 non-empty string
|
||||
* argument and no children; check-kind semantics are out of scope here. */
|
||||
static MunitResult
|
||||
test_feature_checks_structural(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
/* the two fixture kinds plus arbitrary names all pass structurally */
|
||||
assert_valid("project \"p\" version \"1.0\"\n"
|
||||
"feature \"f\" { header \"h.h\"\nlibrary \"l\" }");
|
||||
assert_valid("project \"p\" version \"1.0\"\n"
|
||||
"feature \"f\" { type \"size_t\"\nsizeof \"int\"\n"
|
||||
"function \"strdup\"\nprogram \"gcc\"\n"
|
||||
"compiler_flag \"-Wall\"\npkg_config \"zlib\" }");
|
||||
assert_valid("project \"p\" version \"1.0\"\n"
|
||||
"feature \"f\" { frobnicate \"x\" }");
|
||||
|
||||
/* but a check still needs its argument (a non-empty string) */
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" { header }",
|
||||
"check 'header' requires an argument", 2, 15);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" { header 42 }",
|
||||
"check 'header' argument must be a non-empty string (got int)",
|
||||
2, 22);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" { header \"h.h\" { x } }",
|
||||
"check 'header' takes no children", 2, 30);
|
||||
|
||||
/* feature shapes */
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" \"g\" { header \"h.h\" }",
|
||||
"feature 'f' takes exactly one argument", 2, 13);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\nfeature \"f\" foo=1 { header \"h.h\" }",
|
||||
"feature 'f' has unexpected property 'foo'", 2, 13);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (f) option: `default` is an optional bool property; non-bool defaults
|
||||
* are rejected with the value's span, as are other properties, duplicate
|
||||
* defaults, children, and annotated values. */
|
||||
static MunitResult
|
||||
test_option_default(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
|
||||
assert_valid("project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"debug\" default=#true");
|
||||
assert_valid("project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\"");
|
||||
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" default=\"yes\"",
|
||||
"'default' must be a boolean (got string)", 3, 20);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" default=1",
|
||||
"'default' must be a boolean (got int)", 3, 20);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" default=#null",
|
||||
"'default' must be a boolean (got null)", 3, 20);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" default=(bool)#true",
|
||||
"'default' must not carry a type annotation", 3, 26);
|
||||
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" foo=1",
|
||||
"unexpected property 'foo' (only 'default' is allowed)", 3, 12);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" default=#true default=#false",
|
||||
"duplicate 'default' property", 3, 26);
|
||||
assert_schema_error(
|
||||
"project \"p\" version \"1.0\"\n"
|
||||
"target \"t\" { src \"a.c\" }\n"
|
||||
"option \"x\" { default #true }",
|
||||
"option 'x' takes no children", 3, 14);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitTest tests[] = {
|
||||
{ "/schema/valid-fixture", test_valid_fixture, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/missing-project", test_missing_project, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/project-not-first", test_project_not_first, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/unknown-top-level", test_unknown_top_level, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/duplicate-project", test_duplicate_project, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/missing-args", test_missing_args, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/wrong-arg-types", test_wrong_arg_types, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/project-extras", test_project_extras, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/target-children", test_target_children, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/feature-checks-structural", test_feature_checks_structural,
|
||||
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/schema/option-default", test_option_default, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
};
|
||||
|
||||
static const MunitSuite suite = {
|
||||
"/schema", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
|
||||
};
|
||||
|
||||
int
|
||||
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
|
||||
{
|
||||
return munit_suite_main(&suite, NULL, argc, argv);
|
||||
}
|
||||
Reference in New Issue
Block a user