use std::collections::BTreeSet;
use std::path::Path;
use brink_syntax_native::{SyntaxKind, parse};
enum Coverage {
Exercised,
ExemptUnreachable(&'static str),
}
#[expect(
clippy::too_many_lines,
reason = "one arm per SyntaxKind variant, flat classification table"
)]
#[expect(
clippy::match_same_arms,
reason = "arms are grouped to mirror syntax_kind.rs's own section layout, not merged for brevity — see fn doc"
)]
fn classify(kind: SyntaxKind) -> Coverage {
use Coverage::{ExemptUnreachable, Exercised};
match kind {
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::LINE_COMMENT
| SyntaxKind::BLOCK_COMMENT
| SyntaxKind::DOC_COMMENT_OUTER
| SyntaxKind::DOC_COMMENT_INNER => Exercised,
SyntaxKind::KW_PUB
| SyntaxKind::KW_FLOW
| SyntaxKind::KW_FN
| SyntaxKind::KW_VAR
| SyntaxKind::KW_CONST
| SyntaxKind::KW_LET
| SyntaxKind::KW_FLAGS
| SyntaxKind::KW_STRUCT
| SyntaxKind::KW_EXTERN
| SyntaxKind::KW_IMPORT
| SyntaxKind::KW_USE
| SyntaxKind::KW_MODULE
| SyntaxKind::KW_RETURN
| SyntaxKind::KW_REF
| SyntaxKind::KW_IF
| SyntaxKind::KW_MATCH
| SyntaxKind::KW_ELSE
| SyntaxKind::KW_WHILE
| SyntaxKind::KW_FOR
| SyntaxKind::KW_IN
| SyntaxKind::KW_UNTIL
| SyntaxKind::KW_BREAK
| SyntaxKind::KW_CONTINUE
| SyntaxKind::KW_AS
| SyntaxKind::KW_OR
| SyntaxKind::KW_TRUE
| SyntaxKind::KW_FALSE
| SyntaxKind::KW_END
| SyntaxKind::KW_DONE => Exercised,
SyntaxKind::EQ
| SyntaxKind::PLUS_EQ
| SyntaxKind::MINUS_EQ
| SyntaxKind::EQ_EQ
| SyntaxKind::BANG_EQ
| SyntaxKind::LT
| SyntaxKind::GT
| SyntaxKind::LT_EQ
| SyntaxKind::GT_EQ
| SyntaxKind::AMP
| SyntaxKind::AMP_AMP
| SyntaxKind::PLUS
| SyntaxKind::MINUS
| SyntaxKind::STAR
| SyntaxKind::SLASH
| SyntaxKind::PERCENT
| SyntaxKind::BANG
| SyntaxKind::QUESTION
| SyntaxKind::L_PAREN
| SyntaxKind::R_PAREN
| SyntaxKind::L_BRACE
| SyntaxKind::R_BRACE
| SyntaxKind::L_BRACKET
| SyntaxKind::R_BRACKET
| SyntaxKind::PIPE
| SyntaxKind::COMMA
| SyntaxKind::DOT
| SyntaxKind::COLON
| SyntaxKind::SEMICOLON
| SyntaxKind::COLON_COLON
| SyntaxKind::HASH
| SyntaxKind::TILDE
| SyntaxKind::BACKSLASH
| SyntaxKind::AT => Exercised,
SyntaxKind::STAR_EQ | SyntaxKind::SLASH_EQ => Exercised,
SyntaxKind::CARET => Exercised,
SyntaxKind::AT_L_BRACKET
| SyntaxKind::GLUE
| SyntaxKind::DIVERT
| SyntaxKind::THREAD
| SyntaxKind::FAT_ARROW => Exercised,
SyntaxKind::INTEGER
| SyntaxKind::FLOAT
| SyntaxKind::QUOTE
| SyntaxKind::STRING_TEXT
| SyntaxKind::STRING_ESCAPE
| SyntaxKind::IDENT
| SyntaxKind::ERROR_TOKEN => Exercised,
SyntaxKind::EOF => ExemptUnreachable(
"synthetic lookahead sentinel; lexer never emits it, bump() never pushes it into the tree (parser/mod.rs)",
),
SyntaxKind::DOC_COMMENT => Exercised,
SyntaxKind::SOURCE_FILE
| SyntaxKind::FLOW_DECL
| SyntaxKind::FN_DECL
| SyntaxKind::PARAM_LIST
| SyntaxKind::PARAM
| SyntaxKind::VAR_DECL
| SyntaxKind::CONST_DECL
| SyntaxKind::FLAGS_DECL
| SyntaxKind::FLAGS_MEMBER_LIST
| SyntaxKind::FLAGS_MEMBER
| SyntaxKind::STRUCT_DECL
| SyntaxKind::STRUCT_FIELD
| SyntaxKind::EXTERN_DECL
| SyntaxKind::USE_DECL
| SyntaxKind::USE_TREE
| SyntaxKind::USE_TREE_LIST
| SyntaxKind::IMPORT_DECL
| SyntaxKind::MODULE_DECL => Exercised,
SyntaxKind::BLOCK
| SyntaxKind::CONTENT_LINE
| SyntaxKind::LOGIC_LINE
| SyntaxKind::PROSE_LINE
| SyntaxKind::TEXT
| SyntaxKind::INTERPOLATION
| SyntaxKind::GLUE_NODE
| SyntaxKind::TAG_LINE
| SyntaxKind::TAG => Exercised,
SyntaxKind::SCENE_STITCH
| SyntaxKind::SCENE_HEADING
| SyntaxKind::SCENE_TITLE
| SyntaxKind::SCENE_SLUG
| SyntaxKind::SCENE_BODY
| SyntaxKind::CUE
| SyntaxKind::CUE_NAME
| SyntaxKind::COMPACT_CUE
| SyntaxKind::PARENTHETICAL
| SyntaxKind::BANG_DISPATCH
| SyntaxKind::DISPATCH_NAME => Exercised,
SyntaxKind::SPAN
| SyntaxKind::SPAN_NAME
| SyntaxKind::SPAN_ATTR
| SyntaxKind::SPAN_ATTR_VALUE
| SyntaxKind::ESCAPE => Exercised,
SyntaxKind::CHOICE_POINT
| SyntaxKind::CHOICE
| SyntaxKind::CHOICE_BULLET
| SyntaxKind::LABEL
| SyntaxKind::CHOICE_GUARD
| SyntaxKind::CHOICE_START_CONTENT
| SyntaxKind::CHOICE_BRACKET_CONTENT
| SyntaxKind::CHOICE_INNER_CONTENT
| SyntaxKind::CHOICE_BODY
| SyntaxKind::ELSE_BRANCH
| SyntaxKind::SPLICE => Exercised,
SyntaxKind::CONDITIONAL_BLOCK
| SyntaxKind::IF_ARM
| SyntaxKind::MATCH_ARM
| SyntaxKind::MATCH_PATTERN
| SyntaxKind::ALTERNATION_BLOCK
| SyntaxKind::ALTERNATION_MARKER
| SyntaxKind::ENTRY => Exercised,
SyntaxKind::ANNOTATION_LINE | SyntaxKind::ANNOTATION_ARGS | SyntaxKind::ANNOTATION_ARG => {
Exercised
}
SyntaxKind::DIVERT_STMT
| SyntaxKind::TUNNEL_CALL
| SyntaxKind::DIVERT_TARGET
| SyntaxKind::RETURN_STMT
| SyntaxKind::RETURN_REDIRECT => Exercised,
SyntaxKind::PATH | SyntaxKind::PATH_SEGMENT => Exercised,
SyntaxKind::INTEGER_LIT
| SyntaxKind::FLOAT_LIT
| SyntaxKind::STRING_LIT
| SyntaxKind::BOOLEAN_LIT
| SyntaxKind::PATH_EXPR
| SyntaxKind::PAREN_EXPR
| SyntaxKind::PREFIX_EXPR
| SyntaxKind::INFIX_EXPR
| SyntaxKind::CALL_EXPR
| SyntaxKind::ARG_LIST
| SyntaxKind::LAMBDA_EXPR
| SyntaxKind::LAMBDA_PARAMS => Exercised,
SyntaxKind::ARRAY_LITERAL => Exercised,
SyntaxKind::CONSTRUCT_LITERAL | SyntaxKind::CONSTRUCT_ENTRY => Exercised,
SyntaxKind::STMT_BLOCK
| SyntaxKind::LET_STMT
| SyntaxKind::ASSIGN_STMT
| SyntaxKind::EXPR_STMT => Exercised,
SyntaxKind::BREAK_STMT | SyntaxKind::CONTINUE_STMT => Exercised,
SyntaxKind::IF_STMT
| SyntaxKind::ELSE_CLAUSE
| SyntaxKind::WHILE_STMT
| SyntaxKind::FOR_STMT
| SyntaxKind::UNTIL_STMT => Exercised,
SyntaxKind::TYPE_ANNOTATION
| SyntaxKind::TYPE_EXPR
| SyntaxKind::TYPE_NAME
| SyntaxKind::TYPE_GENERIC
| SyntaxKind::TYPE_FN => Exercised,
SyntaxKind::AS_BINDING => Exercised,
SyntaxKind::ERROR => Exercised,
SyntaxKind::__LAST => {
ExemptUnreachable("rowan discriminant-space sentinel, not a real kind")
}
}
}
fn collect_brink_files(dir: &Path, files: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries {
let Ok(entry) = entry else { continue };
let path = entry.path();
if path.is_dir() {
collect_brink_files(&path, files);
} else if path.extension().is_some_and(|ext| ext == "brink") {
files.push(path);
}
}
}
fn exercised_kinds() -> Result<BTreeSet<SyntaxKind>, String> {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let corpus_dir = manifest_dir.join("tests").join("corpus");
let corpus_dir = corpus_dir
.canonicalize()
.map_err(|e| format!("corpus directory not found: {e}"))?;
let mut files = Vec::new();
collect_brink_files(&corpus_dir, &mut files);
if files.is_empty() {
return Err(format!("no .brink files found in {}", corpus_dir.display()));
}
let mut seen = BTreeSet::new();
for path in &files {
let source =
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
let parsed = parse(&source);
for element in parsed.syntax().descendants_with_tokens() {
seen.insert(element.kind());
}
}
Ok(seen)
}
#[test]
fn every_syntax_kind_is_exercised_or_explicitly_exempt() {
let seen = exercised_kinds().unwrap();
let mut missing = Vec::new();
let mut stale_exemptions = Vec::new();
let mut raw = 0u16;
loop {
if raw == SyntaxKind::__LAST as u16 {
break;
}
let kind = <brink_syntax_native::NativeLanguage as rowan::Language>::kind_from_raw(
rowan::SyntaxKind(raw),
);
raw += 1;
match classify(kind) {
Coverage::Exercised => {
if !seen.contains(&kind) {
missing.push(kind);
}
}
Coverage::ExemptUnreachable(_reason) => {
if seen.contains(&kind) {
stale_exemptions.push(kind);
}
}
}
}
assert!(
stale_exemptions.is_empty(),
"these SyntaxKinds are classified ExemptUnreachable in \
tests/coverage.rs::classify but a corpus fixture now produces \
them — the exemption is stale; reclassify as Exercised (the \
corpus already proves it, no new fixture needed):\n{stale_exemptions:#?}"
);
assert!(
missing.is_empty(),
"the grammar-coverage completeness gate (#1200) found {} \
SyntaxKind variant(s) with no test coverage anywhere in \
tests/corpus/. Each one is either a genuine missing test (add a \
`.brink` fixture, or extend an existing one, that reaches it) or \
a SyntaxKind that is unreachable by construction (classify it \
`ExemptUnreachable` in this file's `classify` function, with a \
comment naming the exact reason — never a blanket allow):\n{missing:#?}",
missing.len()
);
}