use crate::{
core::{Ident, SymbolPath, Value},
partitions::SymbolEntry,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
impl Severity {
pub fn is_error(&self) -> bool {
matches!(self, Severity::Error)
}
pub fn as_str(&self) -> &'static str {
match self {
| Severity::Error => "error",
| Severity::Warning => "warning",
| Severity::Info => "info",
}
}
}
#[derive(Debug, Clone)]
pub struct Error {
pub message: String,
pub span: Option<laburnum::Span>,
pub severity: Severity,
pub context: Vec<String>,
pub suggestions: Vec<String>,
}
impl Error {
pub fn new(message: String, span: Option<laburnum::Span>) -> Self {
Self {
message,
span,
severity: Severity::Error,
context: Vec::new(),
suggestions: Vec::new(),
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_context(mut self, context: String) -> Self {
self.context.push(context);
self
}
pub fn with_suggestion(mut self, suggestion: String) -> Self {
self.suggestions.push(suggestion);
self
}
pub fn has_suggestions(&self) -> bool {
!self.suggestions.is_empty()
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
pub fn is_error(&self) -> bool {
self.severity.is_error()
}
}
pub struct SymbolError<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
kind: SymbolErrorKind<V, I, P>,
}
impl<V, I, P> SymbolError<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
pub fn new(kind: SymbolErrorKind<V, I, P>) -> Self {
Self { kind }
}
pub fn kind(&self) -> &SymbolErrorKind<V, I, P> {
&self.kind
}
pub fn description(&self) -> String {
self.kind.description()
}
pub fn is_recoverable(&self) -> bool {
self.kind.is_recoverable()
}
}
impl<V, I, P> std::fmt::Debug for SymbolError<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SymbolError")
.field("kind", &self.kind)
.finish()
}
}
impl<V, I, P> Clone for SymbolError<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
fn clone(&self) -> Self {
Self {
kind: self.kind.clone(),
}
}
}
#[derive(Debug)]
pub enum SymbolErrorKind<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
NotFound {
name: String,
scope_name: String,
},
AlreadyExists {
name: String,
existing: SymbolEntry<V, I, P>,
},
WrongKind {
name: String,
expected: Vec<String>,
found: String,
},
CircularReference {
chain: Vec<String>,
},
InvalidReference {
from: String,
to: String,
reason: String,
},
SymboldNotAllowedInScope {
scope_kind: String,
name: String,
reason: String,
},
InvalidFields {
name: String,
reason: String,
},
InvalidSymbolId(SymbolEntry<V, I, P>),
SourceKeyMismatch {
expected: laburnum::SourceKey,
actual: laburnum::SourceKey,
},
}
impl<V, I, P> Clone for SymbolErrorKind<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
fn clone(&self) -> Self {
match self {
| Self::NotFound { name, scope_name } => Self::NotFound {
name: name.clone(),
scope_name: scope_name.clone(),
},
| Self::AlreadyExists { name, existing } => Self::AlreadyExists {
name: name.clone(),
existing: *existing,
},
| Self::WrongKind {
name,
expected,
found,
} => Self::WrongKind {
name: name.clone(),
expected: expected.clone(),
found: found.clone(),
},
| Self::CircularReference { chain } => Self::CircularReference {
chain: chain.clone(),
},
| Self::InvalidReference { from, to, reason } => Self::InvalidReference {
from: from.clone(),
to: to.clone(),
reason: reason.clone(),
},
| Self::SymboldNotAllowedInScope {
scope_kind,
name,
reason,
} => Self::SymboldNotAllowedInScope {
scope_kind: scope_kind.clone(),
name: name.clone(),
reason: reason.clone(),
},
| Self::InvalidFields { name, reason } => Self::InvalidFields {
name: name.clone(),
reason: reason.clone(),
},
| Self::InvalidSymbolId(id) => Self::InvalidSymbolId(*id),
| Self::SourceKeyMismatch { expected, actual } => {
Self::SourceKeyMismatch {
expected: *expected,
actual: *actual,
}
},
}
}
}
impl<V, I, P> SymbolErrorKind<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
pub fn description(&self) -> String {
match self {
| SymbolErrorKind::NotFound { name, scope_name } => {
format!("Symbol '{}' not found in scope '{}'", name, scope_name)
},
| SymbolErrorKind::AlreadyExists { name, .. } => {
format!("Symbol '{}' already exists", name)
},
| SymbolErrorKind::WrongKind {
name,
expected,
found,
} => {
if expected.len() == 1 {
format!(
"Symbol '{}' is {} but expected {}",
name, found, expected[0]
)
} else {
format!(
"Symbol '{}' is {} but expected one of: {}",
name,
found,
expected.join(", ")
)
}
},
| SymbolErrorKind::CircularReference { chain } => {
format!("Circular reference detected: {}", chain.join(" -> "))
},
| SymbolErrorKind::InvalidReference { from, to, reason } => {
format!("Invalid reference from '{}' to '{}': {}", from, to, reason)
},
| SymbolErrorKind::SymboldNotAllowedInScope {
scope_kind,
name,
reason,
} => {
format!(
"Symbol '{}' is not allowed in scope '{}': {}",
name, scope_kind, reason
)
},
| SymbolErrorKind::InvalidFields { name, reason } => {
format!("Invalid fields for symbol '{}': {}", name, reason)
},
| SymbolErrorKind::InvalidSymbolId(id) => {
format!("Invalid symbol ID: {:?}", id)
},
| SymbolErrorKind::SourceKeyMismatch { expected, actual } => {
format!("Source key mismatch: expected {}, got {}", expected, actual)
},
}
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
SymbolErrorKind::NotFound { .. }
| SymbolErrorKind::WrongKind { .. }
| SymbolErrorKind::InvalidReference { .. }
)
}
}
impl<V, I, P> SymbolError<V, I, P>
where
V: Value<I>,
I: Ident,
P: SymbolPath,
{
pub fn not_found(name: String, scope_name: String) -> Self {
Self::new(SymbolErrorKind::NotFound { name, scope_name })
}
pub fn already_exists(name: String, existing: SymbolEntry<V, I, P>) -> Self {
Self::new(SymbolErrorKind::AlreadyExists { name, existing })
}
pub fn wrong_kind(
name: String,
expected: Vec<String>,
found: String,
) -> Self {
Self::new(SymbolErrorKind::WrongKind {
name,
expected,
found,
})
}
pub fn circular_reference(chain: Vec<String>) -> Self {
Self::new(SymbolErrorKind::CircularReference { chain })
}
pub fn invalid_reference(from: String, to: String, reason: String) -> Self {
Self::new(SymbolErrorKind::InvalidReference { from, to, reason })
}
pub fn symbol_not_allowed_in_scope(
scope_kind: String,
name: String,
reason: String,
) -> Self {
Self::new(SymbolErrorKind::SymboldNotAllowedInScope {
scope_kind,
name,
reason,
})
}
pub fn invalid_fields(name: String, reason: String) -> Self {
Self::new(SymbolErrorKind::InvalidFields { name, reason })
}
pub fn invalid_symbol_id(id: SymbolEntry<V, I, P>) -> Self {
Self::new(SymbolErrorKind::InvalidSymbolId(id))
}
pub fn source_key_mismatch(
expected: laburnum::SourceKey,
actual: laburnum::SourceKey,
) -> Self {
Self::new(SymbolErrorKind::SourceKeyMismatch { expected, actual })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
#[test]
fn severity_is_error() {
assert!(Severity::Error.is_error());
assert!(!Severity::Warning.is_error());
assert!(!Severity::Info.is_error());
}
#[test]
fn severity_as_str() {
assert_eq!(Severity::Error.as_str(), "error");
assert_eq!(Severity::Warning.as_str(), "warning");
assert_eq!(Severity::Info.as_str(), "info");
}
#[test]
fn error_new_defaults_to_error_severity() {
let err = Error::new("test".to_string(), None);
assert_eq!(err.severity, Severity::Error);
}
#[test]
fn error_with_severity() {
let err =
Error::new("test".to_string(), None).with_severity(Severity::Warning);
assert_eq!(err.severity, Severity::Warning);
}
#[test]
fn error_with_context() {
let err = Error::new("test".to_string(), None)
.with_context("ctx1".to_string())
.with_context("ctx2".to_string());
assert_eq!(err.context.len(), 2);
assert_eq!(err.context[0], "ctx1");
assert_eq!(err.context[1], "ctx2");
}
#[test]
fn error_with_suggestion() {
let err = Error::new("test".to_string(), None)
.with_suggestion("fix1".to_string())
.with_suggestion("fix2".to_string());
assert_eq!(err.suggestions.len(), 2);
assert_eq!(err.suggestions[0], "fix1");
assert_eq!(err.suggestions[1], "fix2");
}
#[test]
fn error_has_suggestions() {
let err = Error::new("test".to_string(), None);
assert!(!err.has_suggestions());
let err = err.with_suggestion("fix".to_string());
assert!(err.has_suggestions());
}
#[test]
fn error_has_context() {
let err = Error::new("test".to_string(), None);
assert!(!err.has_context());
let err = err.with_context("ctx".to_string());
assert!(err.has_context());
}
#[test]
fn not_found_description() {
let err = SymbolError::<DV, SI, TP>::not_found(
"myVar".to_string(),
"global".to_string(),
);
let desc = err.description();
assert!(desc.contains("myVar"));
assert!(desc.contains("global"));
}
#[test]
fn already_exists_description() {
let mut cache = test_span_cache();
let entry = dummy_symbol_entry(&mut cache);
let err = SymbolError::<DV, SI, TP>::already_exists(
"duplicate".to_string(),
entry,
);
let desc = err.description();
assert!(desc.contains("duplicate"));
}
#[test]
fn wrong_kind_description() {
let err = SymbolError::<DV, SI, TP>::wrong_kind(
"foo".to_string(),
vec!["function".to_string()],
"variable".to_string(),
);
let desc = err.description();
assert!(desc.contains("foo"));
assert!(desc.contains("variable"));
assert!(desc.contains("function"));
}
#[test]
fn circular_reference_description() {
let err = SymbolError::<DV, SI, TP>::circular_reference(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
]);
let desc = err.description();
assert!(desc.contains("a -> b -> c"));
}
#[test]
fn invalid_reference_description() {
let err = SymbolError::<DV, SI, TP>::invalid_reference(
"src".to_string(),
"dst".to_string(),
"type mismatch".to_string(),
);
let desc = err.description();
assert!(desc.contains("src"));
assert!(desc.contains("dst"));
assert!(desc.contains("type mismatch"));
}
#[test]
fn symbol_not_allowed_in_scope_description() {
let err = SymbolError::<DV, SI, TP>::symbol_not_allowed_in_scope(
"block".to_string(),
"myFn".to_string(),
"functions cannot be defined here".to_string(),
);
let desc = err.description();
assert!(desc.contains("myFn"));
assert!(desc.contains("block"));
assert!(desc.contains("functions cannot be defined here"));
}
#[test]
fn invalid_fields_description() {
let err = SymbolError::<DV, SI, TP>::invalid_fields(
"widget".to_string(),
"missing required field".to_string(),
);
let desc = err.description();
assert!(desc.contains("widget"));
assert!(desc.contains("missing required field"));
}
#[test]
fn invalid_symbol_id_description() {
let mut cache = test_span_cache();
let entry = dummy_symbol_entry(&mut cache);
let err = SymbolError::<DV, SI, TP>::invalid_symbol_id(entry);
let desc = err.description();
assert!(desc.contains("Invalid symbol ID"));
}
#[test]
fn source_key_mismatch_description() {
let expected = laburnum::SourceKey::new(1, 0);
let actual = laburnum::SourceKey::new(2, 0);
let err =
SymbolError::<DV, SI, TP>::source_key_mismatch(expected, actual);
let desc = err.description();
let expected_str = format!("{}", expected);
let actual_str = format!("{}", actual);
assert!(desc.contains(&expected_str));
assert!(desc.contains(&actual_str));
}
#[test]
fn recoverable_errors() {
let not_found = SymbolError::<DV, SI, TP>::not_found(
"x".to_string(),
"s".to_string(),
);
assert!(not_found.is_recoverable());
let wrong_kind = SymbolError::<DV, SI, TP>::wrong_kind(
"x".to_string(),
vec!["a".to_string()],
"b".to_string(),
);
assert!(wrong_kind.is_recoverable());
let invalid_ref = SymbolError::<DV, SI, TP>::invalid_reference(
"a".to_string(),
"b".to_string(),
"r".to_string(),
);
assert!(invalid_ref.is_recoverable());
}
#[test]
fn non_recoverable_errors() {
let mut cache = test_span_cache();
let entry = dummy_symbol_entry(&mut cache);
let already_exists = SymbolError::<DV, SI, TP>::already_exists(
"x".to_string(),
entry,
);
assert!(!already_exists.is_recoverable());
let circular = SymbolError::<DV, SI, TP>::circular_reference(vec![
"a".to_string(),
]);
assert!(!circular.is_recoverable());
let invalid_fields = SymbolError::<DV, SI, TP>::invalid_fields(
"x".to_string(),
"r".to_string(),
);
assert!(!invalid_fields.is_recoverable());
let invalid_id =
SymbolError::<DV, SI, TP>::invalid_symbol_id(entry);
assert!(!invalid_id.is_recoverable());
let source_mismatch = SymbolError::<DV, SI, TP>::source_key_mismatch(
laburnum::SourceKey::new(1, 0),
laburnum::SourceKey::new(2, 0),
);
assert!(!source_mismatch.is_recoverable());
let not_allowed =
SymbolError::<DV, SI, TP>::symbol_not_allowed_in_scope(
"s".to_string(),
"n".to_string(),
"r".to_string(),
);
assert!(!not_allowed.is_recoverable());
}
}