use crate::syntax::TextRange;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Error,
Warning,
}
impl Severity {
#[inline]
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum DiagnosticCode {
UnexpectedToken,
UnclosedDelimiter,
NestingDepthExceeded,
MissingSeparator,
MissingValue,
TypeMismatch,
MissingRequiredField,
InvalidEnumVariant,
WrongTupleArity,
ValueConstraintViolation,
UnknownField,
}
impl DiagnosticCode {
#[inline]
#[must_use]
pub fn code(self) -> &'static str {
match self {
DiagnosticCode::UnexpectedToken => "RON-P0001",
DiagnosticCode::UnclosedDelimiter => "RON-P0002",
DiagnosticCode::NestingDepthExceeded => "RON-P0003",
DiagnosticCode::MissingSeparator => "RON-P0004",
DiagnosticCode::MissingValue => "RON-P0005",
DiagnosticCode::TypeMismatch => "RON-V0001",
DiagnosticCode::MissingRequiredField => "RON-V0002",
DiagnosticCode::InvalidEnumVariant => "RON-V0003",
DiagnosticCode::WrongTupleArity => "RON-V0004",
DiagnosticCode::ValueConstraintViolation => "RON-V0005",
DiagnosticCode::UnknownField => "RON-V0006",
}
}
#[inline]
#[must_use]
pub fn default_severity(self) -> Severity {
match self {
DiagnosticCode::UnexpectedToken
| DiagnosticCode::UnclosedDelimiter
| DiagnosticCode::NestingDepthExceeded
| DiagnosticCode::MissingSeparator
| DiagnosticCode::MissingValue
| DiagnosticCode::TypeMismatch
| DiagnosticCode::MissingRequiredField
| DiagnosticCode::InvalidEnumVariant
| DiagnosticCode::WrongTupleArity
| DiagnosticCode::ValueConstraintViolation => Severity::Error,
DiagnosticCode::UnknownField => Severity::Warning,
}
}
#[inline]
#[must_use]
pub fn source(self) -> &'static str {
if self.code().starts_with("RON-V") {
"ronin-types"
} else {
"ronin-core"
}
}
}
impl std::fmt::Display for DiagnosticCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Diagnostic {
pub range: TextRange,
pub message: String,
pub severity: Severity,
pub code: DiagnosticCode,
}
impl Diagnostic {
#[inline]
#[must_use]
pub fn new(code: DiagnosticCode, range: TextRange, message: impl Into<String>) -> Self {
Self {
range,
message: message.into(),
severity: code.default_severity(),
code,
}
}
#[inline]
#[must_use]
pub fn code(&self) -> DiagnosticCode {
self.code
}
#[inline]
#[must_use]
pub fn severity(&self) -> Severity {
self.severity
}
#[inline]
#[must_use]
pub fn range(&self) -> TextRange {
self.range
}
#[inline]
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_values_are_stable() {
assert_eq!(Severity::Error.as_str(), "error");
assert_eq!(Severity::Warning.as_str(), "warning");
assert_eq!(Severity::Error.to_string(), "error");
assert!(Severity::Error < Severity::Warning);
}
#[test]
fn codes_are_namespaced_unique_and_have_severity() {
let all = [
DiagnosticCode::UnexpectedToken,
DiagnosticCode::UnclosedDelimiter,
DiagnosticCode::NestingDepthExceeded,
DiagnosticCode::MissingSeparator,
DiagnosticCode::MissingValue,
];
let mut seen = std::collections::BTreeSet::new();
for c in all {
let s = c.code();
assert!(
s.starts_with("RON-P"),
"code {s:?} must be in the RON-P parse namespace"
);
assert_eq!(s.len(), "RON-P0000".len(), "codes are RON-Pxxxx (4 digits)");
assert!(
s["RON-P".len()..].chars().all(|ch| ch.is_ascii_digit()),
"code {s:?} must end in 4 decimal digits"
);
assert!(seen.insert(s), "duplicate code string {s:?}");
let _ = c.default_severity();
assert_eq!(c.to_string(), s);
}
}
#[test]
fn code_strings_are_pinned() {
assert_eq!(DiagnosticCode::UnexpectedToken.code(), "RON-P0001");
assert_eq!(DiagnosticCode::UnclosedDelimiter.code(), "RON-P0002");
assert_eq!(DiagnosticCode::NestingDepthExceeded.code(), "RON-P0003");
assert_eq!(DiagnosticCode::MissingSeparator.code(), "RON-P0004");
assert_eq!(DiagnosticCode::MissingValue.code(), "RON-P0005");
}
const PARSE_CODES: [DiagnosticCode; 5] = [
DiagnosticCode::UnexpectedToken,
DiagnosticCode::UnclosedDelimiter,
DiagnosticCode::NestingDepthExceeded,
DiagnosticCode::MissingSeparator,
DiagnosticCode::MissingValue,
];
const VALIDATION_CODES: [DiagnosticCode; 6] = [
DiagnosticCode::TypeMismatch,
DiagnosticCode::MissingRequiredField,
DiagnosticCode::InvalidEnumVariant,
DiagnosticCode::WrongTupleArity,
DiagnosticCode::ValueConstraintViolation,
DiagnosticCode::UnknownField,
];
#[test]
fn validation_codes_are_namespaced_unique_and_disjoint_from_parse() {
let mut seen = std::collections::BTreeSet::new();
for c in VALIDATION_CODES {
let s = c.code();
assert!(
s.starts_with("RON-V"),
"code {s:?} must be in the RON-V validation namespace"
);
assert_eq!(s.len(), "RON-V0000".len(), "codes are RON-Vxxxx (4 digits)");
assert!(
s["RON-V".len()..].chars().all(|ch| ch.is_ascii_digit()),
"code {s:?} must end in 4 decimal digits"
);
assert!(seen.insert(s), "duplicate validation code string {s:?}");
let _ = c.default_severity();
assert_eq!(c.to_string(), s);
}
}
#[test]
fn all_codes_are_globally_unique() {
let mut seen = std::collections::BTreeSet::new();
for c in PARSE_CODES.into_iter().chain(VALIDATION_CODES) {
assert!(
seen.insert(c.code()),
"duplicate code string {:?} across P+V namespaces",
c.code()
);
}
assert_eq!(
seen.len(),
PARSE_CODES.len() + VALIDATION_CODES.len(),
"combined registry size must equal P + V counts"
);
}
#[test]
fn source_tag_matches_namespace() {
for c in PARSE_CODES {
assert_eq!(
c.source(),
"ronin-core",
"parse code {} must be ronin-core",
c.code()
);
}
for c in VALIDATION_CODES {
assert_eq!(
c.source(),
"ronin-types",
"validation code {} must be ronin-types",
c.code()
);
}
}
#[test]
fn validation_code_strings_are_pinned() {
assert_eq!(DiagnosticCode::TypeMismatch.code(), "RON-V0001");
assert_eq!(DiagnosticCode::MissingRequiredField.code(), "RON-V0002");
assert_eq!(DiagnosticCode::InvalidEnumVariant.code(), "RON-V0003");
assert_eq!(DiagnosticCode::WrongTupleArity.code(), "RON-V0004");
assert_eq!(DiagnosticCode::ValueConstraintViolation.code(), "RON-V0005");
assert_eq!(DiagnosticCode::UnknownField.code(), "RON-V0006");
}
#[test]
fn validation_default_severities_match_policy() {
assert_eq!(
DiagnosticCode::TypeMismatch.default_severity(),
Severity::Error
);
assert_eq!(
DiagnosticCode::MissingRequiredField.default_severity(),
Severity::Error
);
assert_eq!(
DiagnosticCode::InvalidEnumVariant.default_severity(),
Severity::Error
);
assert_eq!(
DiagnosticCode::WrongTupleArity.default_severity(),
Severity::Error
);
assert_eq!(
DiagnosticCode::ValueConstraintViolation.default_severity(),
Severity::Error
);
assert_eq!(
DiagnosticCode::UnknownField.default_severity(),
Severity::Warning
);
}
#[test]
fn new_uses_default_severity() {
let r = TextRange::new(2, 5);
let d = Diagnostic::new(DiagnosticCode::UnexpectedToken, r, "boom");
assert_eq!(d.code(), DiagnosticCode::UnexpectedToken);
assert_eq!(d.severity(), Severity::Error);
assert_eq!(d.range(), r);
assert_eq!(d.message(), "boom");
}
}