use num_enum::TryFromPrimitive;
#[allow(non_camel_case_types)]
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
#[repr(u16)]
#[non_exhaustive]
pub enum SyntaxKind {
BOM,
WHITESPACE,
NEWLINE,
COMMENT,
PERCENT_COMMENT,
SHEBANG,
EMACS_DIRECTIVE,
DATE,
NUMBER,
STRING,
ACCOUNT,
CURRENCY,
TAG,
LINK,
META_KEY,
FLAG,
BOOL_TRUE,
BOOL_FALSE,
NULL_KW,
TXN_KW,
BALANCE_KW,
OPEN_KW,
CLOSE_KW,
COMMODITY_KW,
PAD_KW,
EVENT_KW,
QUERY_KW,
NOTE_KW,
DOCUMENT_KW,
PRICE_KW,
CUSTOM_KW,
OPTION_KW,
INCLUDE_KW,
PLUGIN_KW,
PUSHTAG_KW,
POPTAG_KW,
PUSHMETA_KW,
POPMETA_KW,
PENDING_KW,
L_BRACE,
R_BRACE,
L_DOUBLE_BRACE,
R_DOUBLE_BRACE,
L_BRACE_HASH,
L_PAREN,
R_PAREN,
AT,
AT_AT,
COLON,
COMMA,
TILDE,
PIPE,
PLUS,
MINUS,
STAR,
SLASH,
HASH,
ERROR_TOKEN,
SOURCE_FILE,
ERROR_NODE,
DIRECTIVE,
OPEN_DIRECTIVE,
CLOSE_DIRECTIVE,
BALANCE_DIRECTIVE,
PAD_DIRECTIVE,
EVENT_DIRECTIVE,
QUERY_DIRECTIVE,
NOTE_DIRECTIVE,
DOCUMENT_DIRECTIVE,
PRICE_DIRECTIVE,
COMMODITY_DIRECTIVE,
PUSHTAG_DIRECTIVE,
POPTAG_DIRECTIVE,
PUSHMETA_DIRECTIVE,
POPMETA_DIRECTIVE,
OPTION_DIRECTIVE,
INCLUDE_DIRECTIVE,
PLUGIN_DIRECTIVE,
CUSTOM_DIRECTIVE,
TRANSACTION,
META_ENTRY,
POSTING,
AMOUNT,
COST_SPEC,
PRICE_ANNOTATION,
}
impl SyntaxKind {
#[must_use]
pub const fn is_token(self) -> bool {
matches!(
self,
Self::BOM
| Self::WHITESPACE
| Self::NEWLINE
| Self::COMMENT
| Self::PERCENT_COMMENT
| Self::SHEBANG
| Self::EMACS_DIRECTIVE
| Self::DATE
| Self::NUMBER
| Self::STRING
| Self::ACCOUNT
| Self::CURRENCY
| Self::TAG
| Self::LINK
| Self::META_KEY
| Self::FLAG
| Self::BOOL_TRUE
| Self::BOOL_FALSE
| Self::NULL_KW
| Self::TXN_KW
| Self::BALANCE_KW
| Self::OPEN_KW
| Self::CLOSE_KW
| Self::COMMODITY_KW
| Self::PAD_KW
| Self::EVENT_KW
| Self::QUERY_KW
| Self::NOTE_KW
| Self::DOCUMENT_KW
| Self::PRICE_KW
| Self::CUSTOM_KW
| Self::OPTION_KW
| Self::INCLUDE_KW
| Self::PLUGIN_KW
| Self::PUSHTAG_KW
| Self::POPTAG_KW
| Self::PUSHMETA_KW
| Self::POPMETA_KW
| Self::PENDING_KW
| Self::L_BRACE
| Self::R_BRACE
| Self::L_DOUBLE_BRACE
| Self::R_DOUBLE_BRACE
| Self::L_BRACE_HASH
| Self::L_PAREN
| Self::R_PAREN
| Self::AT
| Self::AT_AT
| Self::COLON
| Self::COMMA
| Self::TILDE
| Self::PIPE
| Self::PLUS
| Self::MINUS
| Self::STAR
| Self::SLASH
| Self::HASH
| Self::ERROR_TOKEN
)
}
#[must_use]
pub const fn is_trivia(self) -> bool {
matches!(
self,
Self::BOM
| Self::WHITESPACE
| Self::NEWLINE
| Self::COMMENT
| Self::PERCENT_COMMENT
| Self::SHEBANG
| Self::EMACS_DIRECTIVE
)
}
}
impl From<SyntaxKind> for rowan::SyntaxKind {
fn from(kind: SyntaxKind) -> Self {
Self(kind as u16)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum BeancountLanguage {}
impl rowan::Language for BeancountLanguage {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
debug_assert!(
SyntaxKind::try_from(raw.0).is_ok(),
"unknown SyntaxKind discriminant {} — cross-version GreenNode \
skew, manifest reorder corruption, or a missing num_enum \
derive update. In release builds this becomes ERROR_NODE.",
raw.0,
);
SyntaxKind::try_from(raw.0).unwrap_or(SyntaxKind::ERROR_NODE)
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.into()
}
}
pub type SyntaxNode = rowan::SyntaxNode<BeancountLanguage>;
pub type SyntaxToken = rowan::SyntaxToken<BeancountLanguage>;
pub type SyntaxElement = rowan::SyntaxElement<BeancountLanguage>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nodes_are_not_tokens() {
let node_kinds = [
SyntaxKind::SOURCE_FILE,
SyntaxKind::ERROR_NODE,
SyntaxKind::DIRECTIVE,
SyntaxKind::OPEN_DIRECTIVE,
SyntaxKind::CLOSE_DIRECTIVE,
SyntaxKind::BALANCE_DIRECTIVE,
SyntaxKind::PAD_DIRECTIVE,
SyntaxKind::EVENT_DIRECTIVE,
SyntaxKind::QUERY_DIRECTIVE,
SyntaxKind::NOTE_DIRECTIVE,
SyntaxKind::DOCUMENT_DIRECTIVE,
SyntaxKind::PRICE_DIRECTIVE,
SyntaxKind::COMMODITY_DIRECTIVE,
SyntaxKind::PUSHTAG_DIRECTIVE,
SyntaxKind::POPTAG_DIRECTIVE,
SyntaxKind::PUSHMETA_DIRECTIVE,
SyntaxKind::POPMETA_DIRECTIVE,
SyntaxKind::OPTION_DIRECTIVE,
SyntaxKind::INCLUDE_DIRECTIVE,
SyntaxKind::PLUGIN_DIRECTIVE,
SyntaxKind::CUSTOM_DIRECTIVE,
SyntaxKind::TRANSACTION,
SyntaxKind::META_ENTRY,
SyntaxKind::POSTING,
SyntaxKind::AMOUNT,
SyntaxKind::COST_SPEC,
SyntaxKind::PRICE_ANNOTATION,
];
for kind in node_kinds {
assert!(
!kind.is_token(),
"{kind:?} is a node but is_token() returns true",
);
}
}
#[test]
fn every_kind_partitions_token_xor_node() {
let all_kinds: Vec<SyntaxKind> = (0u16..=u16::MAX)
.filter_map(|d| SyntaxKind::try_from(d).ok())
.collect();
assert!(
!all_kinds.is_empty(),
"SyntaxKind::try_from rejected every discriminant 0..256",
);
let documented_nodes = [
SyntaxKind::SOURCE_FILE,
SyntaxKind::ERROR_NODE,
SyntaxKind::DIRECTIVE,
SyntaxKind::OPEN_DIRECTIVE,
SyntaxKind::CLOSE_DIRECTIVE,
SyntaxKind::BALANCE_DIRECTIVE,
SyntaxKind::PAD_DIRECTIVE,
SyntaxKind::EVENT_DIRECTIVE,
SyntaxKind::QUERY_DIRECTIVE,
SyntaxKind::NOTE_DIRECTIVE,
SyntaxKind::DOCUMENT_DIRECTIVE,
SyntaxKind::PRICE_DIRECTIVE,
SyntaxKind::COMMODITY_DIRECTIVE,
SyntaxKind::PUSHTAG_DIRECTIVE,
SyntaxKind::POPTAG_DIRECTIVE,
SyntaxKind::PUSHMETA_DIRECTIVE,
SyntaxKind::POPMETA_DIRECTIVE,
SyntaxKind::OPTION_DIRECTIVE,
SyntaxKind::INCLUDE_DIRECTIVE,
SyntaxKind::PLUGIN_DIRECTIVE,
SyntaxKind::CUSTOM_DIRECTIVE,
SyntaxKind::TRANSACTION,
SyntaxKind::META_ENTRY,
SyntaxKind::POSTING,
SyntaxKind::AMOUNT,
SyntaxKind::COST_SPEC,
SyntaxKind::PRICE_ANNOTATION,
];
let observed_nodes: Vec<SyntaxKind> = all_kinds
.iter()
.copied()
.filter(|k| !k.is_token())
.collect();
assert_eq!(
observed_nodes.len(),
documented_nodes.len(),
"is_token() says there are {} node kinds but the \
documented list has {}: observed={observed_nodes:?}, \
documented={documented_nodes:?}. A new SyntaxKind \
variant was added without updating is_token's matches! \
arm AND the documented_nodes list in this test.",
observed_nodes.len(),
documented_nodes.len(),
);
for kind in documented_nodes {
assert!(
observed_nodes.contains(&kind),
"{kind:?} is documented as a node but is_token() \
returns true for it",
);
}
}
#[test]
fn tokens_are_tokens() {
let token_kinds = [
SyntaxKind::BOM,
SyntaxKind::WHITESPACE,
SyntaxKind::NEWLINE,
SyntaxKind::COMMENT,
SyntaxKind::PERCENT_COMMENT,
SyntaxKind::SHEBANG,
SyntaxKind::EMACS_DIRECTIVE,
SyntaxKind::DATE,
SyntaxKind::NUMBER,
SyntaxKind::STRING,
SyntaxKind::ACCOUNT,
SyntaxKind::CURRENCY,
SyntaxKind::TAG,
SyntaxKind::LINK,
SyntaxKind::META_KEY,
SyntaxKind::FLAG,
SyntaxKind::BOOL_TRUE,
SyntaxKind::BOOL_FALSE,
SyntaxKind::NULL_KW,
SyntaxKind::TXN_KW,
SyntaxKind::BALANCE_KW,
SyntaxKind::OPEN_KW,
SyntaxKind::CLOSE_KW,
SyntaxKind::COMMODITY_KW,
SyntaxKind::PAD_KW,
SyntaxKind::EVENT_KW,
SyntaxKind::QUERY_KW,
SyntaxKind::NOTE_KW,
SyntaxKind::DOCUMENT_KW,
SyntaxKind::PRICE_KW,
SyntaxKind::CUSTOM_KW,
SyntaxKind::OPTION_KW,
SyntaxKind::INCLUDE_KW,
SyntaxKind::PLUGIN_KW,
SyntaxKind::PUSHTAG_KW,
SyntaxKind::POPTAG_KW,
SyntaxKind::PUSHMETA_KW,
SyntaxKind::POPMETA_KW,
SyntaxKind::PENDING_KW,
SyntaxKind::L_BRACE,
SyntaxKind::R_BRACE,
SyntaxKind::L_DOUBLE_BRACE,
SyntaxKind::R_DOUBLE_BRACE,
SyntaxKind::L_BRACE_HASH,
SyntaxKind::L_PAREN,
SyntaxKind::R_PAREN,
SyntaxKind::AT,
SyntaxKind::AT_AT,
SyntaxKind::COLON,
SyntaxKind::COMMA,
SyntaxKind::TILDE,
SyntaxKind::PIPE,
SyntaxKind::PLUS,
SyntaxKind::MINUS,
SyntaxKind::STAR,
SyntaxKind::SLASH,
SyntaxKind::HASH,
SyntaxKind::ERROR_TOKEN,
];
for kind in token_kinds {
assert!(
kind.is_token(),
"{kind:?} is a token but is_token() returns false — \
likely missing from the matches! arm in is_token",
);
}
}
#[test]
fn is_trivia_excludes_error_token() {
assert!(!SyntaxKind::ERROR_TOKEN.is_trivia());
assert!(SyntaxKind::ERROR_TOKEN.is_token());
}
#[test]
fn rowan_language_round_trip() {
for kind in [
SyntaxKind::BOM,
SyntaxKind::WHITESPACE,
SyntaxKind::HASH,
SyntaxKind::ERROR_TOKEN,
SyntaxKind::SOURCE_FILE,
SyntaxKind::ERROR_NODE,
] {
let raw: rowan::SyntaxKind = kind.into();
let back = <BeancountLanguage as rowan::Language>::kind_from_raw(raw);
assert_eq!(kind, back);
}
}
}