use alloc::borrow::Cow;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use facet_core::{Def, Shape, Type, UserType};
use facet_pretty::{
FormattedShape, PathSegment as PrettyPathSegment, ShapeFormatConfig,
format_shape_with_spans_and_config,
};
use miette::{Diagnostic, LabeledSpan, NamedSource, Report, SourceSpan};
use crate::{Path, PathStep};
pub fn install_highlighter() {
let _ = miette_arborium::install_global();
}
#[derive(Debug)]
struct TypeDiagnostic {
message: String,
source: NamedSource<String>,
labels: Vec<LabeledSpan>,
}
impl core::fmt::Display for TypeDiagnostic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for TypeDiagnostic {}
impl Diagnostic for TypeDiagnostic {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.source)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
if self.labels.is_empty() {
None
} else {
Some(Box::new(self.labels.iter().cloned()))
}
}
}
#[derive(Debug)]
pub struct PathDiagnostic {
message: String,
source: NamedSource<String>,
labels: Vec<LabeledSpan>,
help: Option<String>,
related: Vec<TypeDiagnostic>,
}
impl core::fmt::Display for PathDiagnostic {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for PathDiagnostic {}
impl Diagnostic for PathDiagnostic {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.source)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
if self.labels.is_empty() {
None
} else {
Some(Box::new(self.labels.iter().cloned()))
}
}
fn help<'a>(&'a self) -> Option<Box<dyn core::fmt::Display + 'a>> {
self.help
.as_ref()
.map(|h| Box::new(h.as_str()) as Box<dyn core::fmt::Display>)
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
if self.related.is_empty() {
None
} else {
Some(Box::new(self.related.iter().map(|d| d as &dyn Diagnostic)))
}
}
}
struct PathSegment {
shape: &'static Shape,
local_path: Vec<PrettyPathSegment>,
}
fn is_displayable_user_type(shape: &Shape) -> bool {
match shape.def {
Def::Option(_) | Def::List(_) | Def::Map(_) | Def::Array(_) | Def::Slice(_) => false,
_ => matches!(
shape.ty,
Type::User(UserType::Struct(_) | UserType::Enum(_))
),
}
}
impl Path {
fn collect_type_segments(&self, root_shape: &'static Shape) -> Vec<PathSegment> {
let mut segments: Vec<PathSegment> = Vec::new();
let mut current_shape = root_shape;
let mut local_path: Vec<PrettyPathSegment> = Vec::new();
let mut current_segment_shape = root_shape;
for step in self.steps() {
match step {
PathStep::Field(idx) => {
let idx = *idx as usize;
if let Type::User(UserType::Struct(sd)) = current_shape.ty {
if let Some(field) = sd.fields.get(idx) {
local_path.push(PrettyPathSegment::Field(Cow::Borrowed(field.name)));
current_shape = field.shape();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
} else if let Type::User(UserType::Enum(ed)) = current_shape.ty {
if let Some(PrettyPathSegment::Variant(variant_name)) = local_path.last()
&& let Some(variant) =
ed.variants.iter().find(|v| v.name == variant_name.as_ref())
&& let Some(field) = variant.data.fields.get(idx)
{
local_path.push(PrettyPathSegment::Field(Cow::Borrowed(field.name)));
current_shape = field.shape();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
}
}
PathStep::Index(_idx) => {
match current_shape.def {
Def::List(ld) => {
current_shape = ld.t();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
Def::Array(ad) => {
current_shape = ad.t();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
Def::Slice(sd) => {
current_shape = sd.t();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
_ => {}
}
}
PathStep::Variant(idx) => {
let idx = *idx as usize;
if let Type::User(UserType::Enum(ed)) = current_shape.ty
&& let Some(variant) = ed.variants.get(idx)
{
local_path.push(PrettyPathSegment::Variant(Cow::Borrowed(variant.name)));
}
}
PathStep::MapKey => {
if let Def::Map(md) = current_shape.def {
current_shape = md.k();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
}
PathStep::MapValue => {
if let Def::Map(md) = current_shape.def {
current_shape = md.v();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
}
PathStep::OptionSome => {
if let Def::Option(od) = current_shape.def {
current_shape = od.t();
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
}
PathStep::Deref => {
if let Def::Pointer(pd) = current_shape.def
&& let Some(pointee) = pd.pointee()
{
current_shape = pointee;
if is_displayable_user_type(current_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path: core::mem::take(&mut local_path),
});
current_segment_shape = current_shape;
}
}
}
}
}
if !local_path.is_empty() || is_displayable_user_type(current_segment_shape) {
segments.push(PathSegment {
shape: current_segment_shape,
local_path,
});
}
segments
}
pub fn to_diagnostic(
&self,
shape: &'static Shape,
message: impl Into<String>,
help: Option<String>,
leaf_field: Option<&'static str>,
) -> PathDiagnostic {
let segments = self.collect_type_segments(shape);
let message = message.into();
let mut diagnostics: Vec<(NamedSource<String>, Vec<LabeledSpan>, String)> = Vec::new();
let config = ShapeFormatConfig::new()
.with_third_party_attrs()
.without_nested_types();
for (i, segment) in segments.iter().rev().enumerate() {
let FormattedShape {
text,
spans,
type_name_span,
} = format_shape_with_spans_and_config(segment.shape, &config);
let source_name = alloc::format!("{}.rs", segment.shape.type_identifier);
let source = NamedSource::new(source_name, text.clone());
let mut labels = Vec::new();
if let Some((start, end)) = type_name_span {
let type_span = SourceSpan::new(start.into(), end - start);
labels.push(LabeledSpan::new_with_span(None, type_span));
}
let is_first = i == 0;
let label_text = if is_first {
"as requested here"
} else {
"via this field"
};
let lookup_path = if let (true, Some(field)) = (is_first, leaf_field) {
let mut path = segment.local_path.clone();
path.push(PrettyPathSegment::Field(Cow::Borrowed(field)));
path
} else {
segment.local_path.clone()
};
let field_span = if let Some(field_span) = spans.get(&lookup_path) {
SourceSpan::new(field_span.key.0.into(), field_span.key.1 - field_span.key.0)
} else {
SourceSpan::new(0.into(), text.len())
};
labels.push(LabeledSpan::new_with_span(
Some(label_text.to_string()),
field_span,
));
let diag_message = if is_first {
message.clone()
} else {
alloc::format!("in type `{}`", segment.shape.type_identifier)
};
diagnostics.push((source, labels, diag_message));
}
let (source, labels, _primary_message) = diagnostics.remove(0);
let related: Vec<TypeDiagnostic> = diagnostics
.into_iter()
.map(|(source, labels, msg)| TypeDiagnostic {
message: msg,
source,
labels,
})
.collect();
PathDiagnostic {
message,
source,
labels,
help,
related,
}
}
pub fn format_pretty(
&self,
shape: &'static Shape,
message: impl Into<String>,
help: Option<String>,
) -> String {
self.format_pretty_impl(shape, message, help, true)
}
pub fn format_pretty_no_color(
&self,
shape: &'static Shape,
message: impl Into<String>,
help: Option<String>,
) -> String {
self.format_pretty_impl(shape, message, help, false)
}
fn format_pretty_impl(
&self,
shape: &'static Shape,
message: impl Into<String>,
help: Option<String>,
use_color: bool,
) -> String {
use miette::{GraphicalReportHandler, GraphicalTheme};
let diagnostic = self.to_diagnostic(shape, message, help, None);
if use_color {
let report = Report::new(diagnostic);
format!("{:?}", report)
} else {
let mut output = String::new();
let handler = GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor());
handler.render_report(&mut output, &diagnostic).unwrap();
output
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
#[test]
fn test_diagnostic_for_struct_field() {
#[derive(Facet)]
#[allow(dead_code)]
struct Config {
name: String,
max_retries: u8,
enabled: bool,
}
let mut path = Path::new();
path.push(PathStep::Field(1));
let output = path.format_pretty(
Config::SHAPE,
"unsupported scalar type",
Some("consider using a different type".to_string()),
);
assert!(output.contains("Config"), "Should mention type name");
assert!(
output.contains("max_retries") || output.contains("u8"),
"Should highlight the field or its type"
);
}
#[test]
fn test_diagnostic_for_nested_type() {
#[derive(Facet)]
#[allow(dead_code)]
struct Inner {
value: i32,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Outer {
name: String,
inner: Inner,
}
let mut path = Path::new();
path.push(PathStep::Field(1)); path.push(PathStep::Field(0));
let output = path.format_pretty(Outer::SHAPE, "nested type error", None);
assert!(output.contains("Outer"), "Should show Outer type: {output}");
assert!(output.contains("Inner"), "Should show Inner type: {output}");
assert!(
output.contains("inner"),
"Should highlight inner field: {output}"
);
assert!(
output.contains("value"),
"Should highlight value field: {output}"
);
}
#[test]
fn test_collect_segments_through_vec() {
#[derive(Facet)]
#[allow(dead_code)]
struct Item {
id: u32,
type_info: u64,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Container {
items: Vec<Item>,
}
let mut path = Path::new();
path.push(PathStep::Field(0)); path.push(PathStep::Index(0)); path.push(PathStep::Field(1));
let segments = path.collect_type_segments(Container::SHAPE);
assert_eq!(
segments.len(),
2,
"Expected 2 segments, got {}",
segments.len()
);
assert_eq!(segments[0].shape.type_identifier, "Container");
assert_eq!(segments[0].local_path.len(), 1);
assert!(
matches!(&segments[0].local_path[0], PrettyPathSegment::Field(name) if name == "items")
);
assert_eq!(segments[1].shape.type_identifier, "Item");
assert_eq!(segments[1].local_path.len(), 1);
assert!(
matches!(&segments[1].local_path[0], PrettyPathSegment::Field(name) if name == "type_info")
);
}
#[test]
fn test_collect_segments_through_option() {
#[derive(Facet)]
#[allow(dead_code)]
struct Inner {
value: i32,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Config {
name: String,
inner: Option<Inner>,
}
let mut path = Path::new();
path.push(PathStep::Field(1)); path.push(PathStep::OptionSome); path.push(PathStep::Field(0));
let segments = path.collect_type_segments(Config::SHAPE);
assert_eq!(
segments.len(),
2,
"Expected 2 segments, got {}",
segments.len()
);
assert_eq!(segments[0].shape.type_identifier, "Config");
assert_eq!(segments[1].shape.type_identifier, "Inner");
}
}