Skip to main content

weaveffi_core/validate/
diagnostic.rs

1//! [`ValidationDiagnostic`]: a [`ValidationError`] plus an optional source
2//! snippet and best-effort span for fancy [`miette`] rendering.
3
4use super::ValidationError;
5use miette::{Diagnostic, NamedSource, SourceSpan};
6
7/// Diagnostic wrapper that attaches an optional source code snippet and a
8/// best-effort byte range to a [`ValidationError`] for fancy rendering via
9/// [`miette`]. The wrapper delegates `help()` and `code()` to the inner error
10/// while exposing its own `source_code` and `labels` so the renderer can
11/// underline the offending identifier in the input.
12#[derive(Debug)]
13pub struct ValidationDiagnostic {
14    /// The underlying validation error being rendered.
15    pub error: ValidationError,
16    /// Named source snippet (filename plus contents), when an on-disk IDL was
17    /// supplied. `None` for in-memory APIs.
18    pub src: Option<NamedSource<String>>,
19    /// Best-effort byte range of the offending identifier within `src`, used
20    /// to underline it. `None` when no span could be located.
21    pub span: Option<SourceSpan>,
22}
23
24impl std::fmt::Display for ValidationDiagnostic {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        std::fmt::Display::fmt(&self.error, f)
27    }
28}
29
30impl std::error::Error for ValidationDiagnostic {
31    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32        self.error.source()
33    }
34}
35
36impl Diagnostic for ValidationDiagnostic {
37    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
38        self.error.code()
39    }
40
41    fn severity(&self) -> Option<miette::Severity> {
42        self.error.severity()
43    }
44
45    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
46        self.error.help()
47    }
48
49    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
50        self.error.url()
51    }
52
53    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
54        self.src
55            .as_ref()
56            .map(|s| s as &dyn miette::SourceCode)
57            .or_else(|| self.error.source_code())
58    }
59
60    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
61        if let Some(span) = self.span {
62            Some(Box::new(std::iter::once(
63                miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
64            )))
65        } else {
66            self.error.labels()
67        }
68    }
69}
70
71impl ValidationDiagnostic {
72    /// Build a [`ValidationDiagnostic`] from a [`ValidationError`] and an
73    /// optional `(filename, contents)` source. When a source is provided the
74    /// constructor performs a best-effort search for the offending identifier
75    /// (e.g. a duplicate module name or unknown type reference) and attaches
76    /// a [`SourceSpan`] for fancy rendering. If no span can be computed the
77    /// label is omitted and miette still produces a nicer message + help
78    /// section than plain `Display`.
79    pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self {
80        let (src, span) = match source {
81            Some((filename, contents)) => {
82                let span = find_offending_span(&error, contents);
83                (Some(NamedSource::new(filename, contents.to_string())), span)
84            }
85            None => (None, None),
86        };
87        Self { error, src, span }
88    }
89}
90
91fn find_offending_span(err: &ValidationError, src: &str) -> Option<SourceSpan> {
92    let needle: &str = match err {
93        ValidationError::DuplicateModuleName(n) => Some(n.as_str()),
94        ValidationError::InvalidModuleName(n, _) => Some(n.as_str()),
95        ValidationError::DuplicateFunctionName { function, .. } => Some(function.as_str()),
96        ValidationError::DuplicateParamName { param, .. } => Some(param.as_str()),
97        ValidationError::ReservedKeyword(n) => Some(n.as_str()),
98        ValidationError::InvalidIdentifier(n, _) => Some(n.as_str()),
99        ValidationError::DuplicateErrorName { name, .. } => Some(name.as_str()),
100        ValidationError::InvalidErrorCode { name, .. } => Some(name.as_str()),
101        ValidationError::NameCollisionWithErrorDomain { name, .. } => Some(name.as_str()),
102        ValidationError::DuplicateStructName { name, .. } => Some(name.as_str()),
103        ValidationError::DuplicateStructField { field, .. } => Some(field.as_str()),
104        ValidationError::EmptyStruct { name, .. } => Some(name.as_str()),
105        ValidationError::DuplicateEnumName { name, .. } => Some(name.as_str()),
106        ValidationError::EmptyEnum { name, .. } => Some(name.as_str()),
107        ValidationError::DuplicateEnumVariant { variant, .. } => Some(variant.as_str()),
108        ValidationError::UnknownTypeRef { name } => Some(name.as_str()),
109        ValidationError::DuplicateCallbackName { name, .. } => Some(name.as_str()),
110        ValidationError::UnsupportedCallbackParamType { param, .. } => Some(param.as_str()),
111        ValidationError::ListenerCallbackNotFound { callback, .. } => Some(callback.as_str()),
112        ValidationError::DuplicateListenerName { name, .. } => Some(name.as_str()),
113        ValidationError::BuilderStructEmpty { name, .. } => Some(name.as_str()),
114        ValidationError::UnsupportedSchemaVersion { version, .. } => Some(version.as_str()),
115        ValidationError::AsyncIteratorReturn { function, .. } => Some(function.as_str()),
116        ValidationError::DuplicateInterfaceName { name, .. } => Some(name.as_str()),
117        ValidationError::DuplicateInterfaceMember { name, .. } => Some(name.as_str()),
118        ValidationError::EmptyInterface { name, .. } => Some(name.as_str()),
119        ValidationError::ConstructorHasReturn { constructor, .. } => Some(constructor.as_str()),
120        ValidationError::AsyncConstructor { constructor, .. } => Some(constructor.as_str()),
121        ValidationError::InterfaceInInvalidPosition { name, .. } => Some(name.as_str()),
122        ValidationError::DuplicateTypeName { name, .. } => Some(name.as_str()),
123        ValidationError::DuplicateErrorCodeName { name, .. } => Some(name.as_str()),
124        ValidationError::ThrowsWithoutErrorDomain { function, .. } => Some(function.as_str()),
125        ValidationError::AbiSymbolCollision { symbol, .. } => Some(symbol.as_str()),
126        _ => None,
127    }?;
128    let quoted = format!("\"{needle}\"");
129    if let Some(pos) = src.find(&quoted) {
130        return Some(SourceSpan::new(pos.into(), quoted.len()));
131    }
132    src.find(needle)
133        .map(|pos| SourceSpan::new(pos.into(), needle.len()))
134}