use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, gen_platform::IsVariant,
)]
pub enum Semantic {
Keyword,
Symbol,
KeywordArg,
String,
Number,
Literal,
Comment,
Accent,
Muted,
Error,
Warning,
Info,
Hint,
Added,
Removed,
Unchanged,
}
impl Semantic {
pub const ALL: &'static [Self] = &[
Self::Keyword,
Self::Symbol,
Self::KeywordArg,
Self::String,
Self::Number,
Self::Literal,
Self::Comment,
Self::Accent,
Self::Muted,
Self::Error,
Self::Warning,
Self::Info,
Self::Hint,
Self::Added,
Self::Removed,
Self::Unchanged,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Keyword => "keyword",
Self::Symbol => "symbol",
Self::KeywordArg => "keyword-arg",
Self::String => "string",
Self::Number => "number",
Self::Literal => "literal",
Self::Comment => "comment",
Self::Accent => "accent",
Self::Muted => "muted",
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
Self::Hint => "hint",
Self::Added => "added",
Self::Removed => "removed",
Self::Unchanged => "unchanged",
}
}
}
impl std::fmt::Display for Semantic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for Semantic {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_all_enumerates_every_variant_in_declaration_order() {
assert_eq!(
Semantic::ALL,
&[
Semantic::Keyword,
Semantic::Symbol,
Semantic::KeywordArg,
Semantic::String,
Semantic::Number,
Semantic::Literal,
Semantic::Comment,
Semantic::Accent,
Semantic::Muted,
Semantic::Error,
Semantic::Warning,
Semantic::Info,
Semantic::Hint,
Semantic::Added,
Semantic::Removed,
Semantic::Unchanged,
],
);
for variant in Semantic::ALL {
let row = [
variant.is_keyword(),
variant.is_symbol(),
variant.is_keyword_arg(),
variant.is_string(),
variant.is_number(),
variant.is_literal(),
variant.is_comment(),
variant.is_accent(),
variant.is_muted(),
variant.is_error(),
variant.is_warning(),
variant.is_info(),
variant.is_hint(),
variant.is_added(),
variant.is_removed(),
variant.is_unchanged(),
];
let hits = row.iter().filter(|b| **b).count();
assert_eq!(
hits, 1,
"Semantic::{variant:?} must satisfy exactly one of the \
15 is_* arm-discriminator predicates; got {row:?}",
);
}
}
#[test]
fn semantic_is_variant_predicates_partition_the_arm_set() {
for (idx, variant) in Semantic::ALL.iter().enumerate() {
let observed: [bool; 16] = [
variant.is_keyword(),
variant.is_symbol(),
variant.is_keyword_arg(),
variant.is_string(),
variant.is_number(),
variant.is_literal(),
variant.is_comment(),
variant.is_accent(),
variant.is_muted(),
variant.is_error(),
variant.is_warning(),
variant.is_info(),
variant.is_hint(),
variant.is_added(),
variant.is_removed(),
variant.is_unchanged(),
];
let mut expected = [false; 16];
expected[idx] = true;
assert_eq!(
observed, expected,
"Semantic::{variant:?} at ALL[{idx}] is_* predicates \
must fire only on their own arm lane (identity \
diagonal); got {observed:?}",
);
}
}
#[test]
fn semantic_as_str_returns_canonical_kebab_case_per_arm() {
assert_eq!(Semantic::Keyword.as_str(), "keyword");
assert_eq!(Semantic::Symbol.as_str(), "symbol");
assert_eq!(Semantic::KeywordArg.as_str(), "keyword-arg");
assert_eq!(Semantic::String.as_str(), "string");
assert_eq!(Semantic::Number.as_str(), "number");
assert_eq!(Semantic::Literal.as_str(), "literal");
assert_eq!(Semantic::Comment.as_str(), "comment");
assert_eq!(Semantic::Accent.as_str(), "accent");
assert_eq!(Semantic::Muted.as_str(), "muted");
assert_eq!(Semantic::Error.as_str(), "error");
assert_eq!(Semantic::Warning.as_str(), "warning");
assert_eq!(Semantic::Info.as_str(), "info");
assert_eq!(Semantic::Hint.as_str(), "hint");
assert_eq!(Semantic::Added.as_str(), "added");
assert_eq!(Semantic::Removed.as_str(), "removed");
assert_eq!(Semantic::Unchanged.as_str(), "unchanged");
}
#[test]
fn semantic_as_str_projections_are_all_distinct_across_arms() {
let mut projections: Vec<&'static str> = Semantic::ALL.iter().map(|s| s.as_str()).collect();
let before = projections.len();
projections.sort_unstable();
projections.dedup();
assert_eq!(
projections.len(),
before,
"Semantic::as_str must be injective across ALL — collisions: \
{projections:?}",
);
}
#[test]
fn semantic_display_and_as_ref_str_route_through_as_str_accessor() {
for &sem in Semantic::ALL {
let via_as_str: &str = sem.as_str();
let via_display: String = format!("{sem}");
let via_as_ref: &str = <Semantic as AsRef<str>>::as_ref(&sem);
assert_eq!(
via_display, via_as_str,
"Semantic::{sem:?} — Display routes off `as_str`; got \
Display={via_display:?} vs as_str={via_as_str:?}",
);
assert_eq!(
via_as_ref, via_as_str,
"Semantic::{sem:?} — AsRef<str> routes off `as_str`; got \
AsRef={via_as_ref:?} vs as_str={via_as_str:?}",
);
}
}
#[test]
fn semantic_as_str_is_usable_in_const_context() {
const KEYWORD: &str = Semantic::Keyword.as_str();
const ERROR: &str = Semantic::Error.as_str();
const UNCHANGED: &str = Semantic::Unchanged.as_str();
const { assert!(KEYWORD.as_bytes()[0] == b'k') };
const { assert!(ERROR.as_bytes()[0] == b'e') };
const { assert!(UNCHANGED.as_bytes()[0] == b'u') };
}
#[test]
fn semantic_is_variant_predicates_are_const_fn() {
const { assert!(Semantic::Keyword.is_keyword()) };
const { assert!(Semantic::Error.is_error()) };
const { assert!(Semantic::Added.is_added()) };
const { assert!(Semantic::Unchanged.is_unchanged()) };
}
}