use super::ValidationError;
#[cfg(feature = "idl")]
use miette::{Diagnostic, NamedSource, SourceSpan};
#[derive(Debug)]
pub struct ValidationDiagnostic {
pub error: ValidationError,
#[cfg(feature = "idl")]
pub src: Option<NamedSource<String>>,
#[cfg(feature = "idl")]
pub span: Option<SourceSpan>,
}
impl std::fmt::Display for ValidationDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.error, f)
}
}
impl std::error::Error for ValidationDiagnostic {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.error.source()
}
}
#[cfg(feature = "idl")]
impl Diagnostic for ValidationDiagnostic {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.error.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.error.url()
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.src
.as_ref()
.map(|s| s as &dyn miette::SourceCode)
.or_else(|| self.error.source_code())
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
if let Some(span) = self.span {
Some(Box::new(std::iter::once(
miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
)))
} else {
self.error.labels()
}
}
}
impl ValidationDiagnostic {
#[cfg(feature = "idl")]
pub(crate) fn new(
error: ValidationError,
scope: &[String],
source: Option<(&str, &str)>,
) -> Self {
let (src, span) = match source {
Some((filename, contents)) => {
let span = error
.needle()
.and_then(|needle| locate(contents, scope, needle));
(Some(NamedSource::new(filename, contents.to_string())), span)
}
None => (None, None),
};
Self { error, src, span }
}
#[cfg(not(feature = "idl"))]
pub(crate) fn new(
error: ValidationError,
_scope: &[String],
_source: Option<(&str, &str)>,
) -> Self {
Self { error }
}
}
#[cfg(feature = "idl")]
#[derive(Clone, Copy)]
enum Needle<'a> {
Decl(&'a str),
Second(&'a str),
Text(&'a str),
Scope,
SecondScope,
}
#[cfg(feature = "idl")]
impl ValidationError {
fn needle(&self) -> Option<Needle<'_>> {
use Needle::{Decl, Scope, Second, SecondScope, Text};
fn duplicate<'a>(name: &'a str, first: &str, second: &str) -> Needle<'a> {
if first == second {
Second(name)
} else {
Decl(name)
}
}
Some(match self {
Self::UnsupportedSchemaVersion { version, .. } => Text(version),
Self::DuplicateModuleName { .. } => SecondScope,
Self::InvalidModuleName { name, .. }
| Self::ReservedKeyword { name }
| Self::InvalidIdentifier { name, .. }
| Self::NameCollisionWithErrorDomain { name, .. }
| Self::InvalidErrorCode { name, .. }
| Self::EmptyStruct { name, .. }
| Self::EmptyEnum { name, .. }
| Self::EmptyInterface { name, .. }
| Self::EmptyCallbackInterface { name, .. } => Decl(name),
Self::DuplicateTypeName {
name,
first,
second,
}
| Self::DuplicateFunctionName {
name,
first,
second,
}
| Self::DuplicateErrorCodeName {
name,
first,
second,
} => duplicate(name, first, second),
Self::DuplicateParamName { param: name, .. }
| Self::DuplicateStructField { field: name, .. }
| Self::DuplicateEnumVariant { variant: name, .. }
| Self::DuplicateEnumVariantField { field: name, .. }
| Self::DuplicateInterfaceMember { name, .. }
| Self::DuplicateCallbackMethod { name, .. } => Second(name),
Self::CancellableNotAsync { .. }
| Self::ThrowsWithoutErrorDomain { .. }
| Self::AsyncIteratorReturn { .. } => Scope,
Self::DuplicateEnumValue { enum_name, .. } => Decl(enum_name),
Self::ConstructorHasReturn { constructor, .. }
| Self::AsyncConstructor { constructor, .. } => Decl(constructor),
Self::InvalidCallbackMethod { method, .. } => Decl(method),
Self::UnknownTypeRef { name }
| Self::QualifiedTypeRef { name }
| Self::UnsupportedPrimitive { name }
| Self::InterfaceInInvalidPosition { name, .. }
| Self::CallbackInterfaceInInvalidPosition { name, .. } => Text(name),
Self::InvalidMapKey { key_type } => Text(key_type),
Self::IteratorInInvalidPosition { .. } => Text("iter"),
Self::NoModuleName
| Self::SymbolCollision { .. }
| Self::ErrorDomainMissingName { .. }
| Self::DuplicateErrorCode { .. } => return None,
})
}
}
#[cfg(feature = "idl")]
fn locate(src: &str, scope: &[String], needle: Needle<'_>) -> Option<SourceSpan> {
let (scope, needle) = match (needle, scope.split_last()) {
(Needle::Scope, Some((last, outer))) => (outer, Needle::Decl(last)),
(Needle::SecondScope, Some((last, outer))) => (outer, Needle::Second(last)),
(Needle::Scope | Needle::SecondScope, None) => return None,
_ => (scope, needle),
};
let mut from = 0;
for anchor in scope {
if let Some(at) = find_decl(src, from, anchor) {
from = at + anchor.len();
}
}
let (text, scoped) = match needle {
Needle::Decl(name) => (name, find_decl(src, from, name)),
Needle::Second(name) => (
name,
find_decl(src, from, name)
.and_then(|first| find_decl(src, first + name.len(), name))
.or_else(|| find_decl(src, from, name)),
),
Needle::Text(text) => (text, find_token(src, from, text)),
Needle::Scope | Needle::SecondScope => unreachable!("resolved above"),
};
let at = scoped.or_else(|| find_token(src, 0, text))?;
Some(span(src, at, text.len()))
}
#[cfg(feature = "idl")]
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
#[cfg(feature = "idl")]
fn find_token(src: &str, from: usize, needle: &str) -> Option<usize> {
if needle.is_empty() {
return None;
}
src.get(from..)?
.match_indices(needle)
.map(|(i, _)| from + i)
.find(|&at| {
let before = src[..at].chars().next_back();
let after = src[at + needle.len()..].chars().next();
!before.is_some_and(is_ident_char) && !after.is_some_and(is_ident_char)
})
}
#[cfg(feature = "idl")]
fn find_decl(src: &str, from: usize, name: &str) -> Option<usize> {
let mut at = from;
loop {
let hit = find_token(src, at, name)?;
let before = src[..hit].trim_end_matches(['"', '\'']).trim_end();
if let Some(key) = before.strip_suffix([':', '=']) {
let key = key.trim_end().trim_end_matches(['"', '\'']);
if key
.strip_suffix("name")
.is_some_and(|rest| !rest.chars().next_back().is_some_and(is_ident_char))
{
return Some(hit);
}
}
at = hit + name.len();
}
}
#[cfg(feature = "idl")]
fn span(src: &str, at: usize, len: usize) -> SourceSpan {
let quote = src[..at]
.chars()
.next_back()
.filter(|c| matches!(c, '"' | '\''));
match quote {
Some(q) if src[at + len..].starts_with(q) => SourceSpan::new((at - 1).into(), len + 2),
_ => SourceSpan::new(at.into(), len),
}
}