use crate::parser::FileId;
use crate::parser::LineColumn;
use crate::parser::SourceFile;
use crate::parser::SourceMap;
use crate::parser::SourceSpan;
use crate::response::GraphQLError;
#[cfg(doc)]
use crate::ExecutableDocument;
#[cfg(doc)]
use crate::Schema;
use ariadne::ColorGenerator;
use ariadne::ReportKind;
use std::cell::Cell;
use std::fmt;
use std::io;
use std::ops::Range;
use std::sync::Arc;
use std::sync::OnceLock;
pub struct Diagnostic<'s, T>
where
T: ToCliReport,
{
pub sources: &'s SourceMap,
pub error: &'s T,
}
pub struct CliReport<'s> {
sources: &'s SourceMap,
colors: ColorGenerator,
report: ariadne::ReportBuilder<'static, AriadneSpan>,
}
#[derive(Debug, Clone, Copy)]
pub enum Color {
Never,
StderrIsTerminal,
}
pub trait ToCliReport: fmt::Display {
fn location(&self) -> Option<SourceSpan>;
fn report(&self, report: &mut CliReport<'_>);
fn to_report<'s>(&self, sources: &'s SourceMap, color: Color) -> CliReport<'s> {
let mut report = CliReport::builder(sources, self.location(), color);
report.with_message(self);
self.report(&mut report);
report
}
fn to_diagnostic<'s>(&'s self, sources: &'s SourceMap) -> Diagnostic<'s, Self>
where
Self: Sized,
{
Diagnostic {
sources,
error: self,
}
}
}
impl<T: ToCliReport> ToCliReport for &T {
fn location(&self) -> Option<SourceSpan> {
ToCliReport::location(*self)
}
fn report(&self, report: &mut CliReport) {
ToCliReport::report(*self, report)
}
}
type AriadneSpan = (FileId, Range<usize>);
fn to_span(location: SourceSpan) -> Option<AriadneSpan> {
let start = location.offset();
let end = location.end_offset();
Some((location.file_id, start..end))
}
struct WriteToFormatter<'a, 'b> {
f: &'a mut fmt::Formatter<'b>,
}
impl io::Write for WriteToFormatter<'_, '_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let s = std::str::from_utf8(buf).map_err(|_| io::ErrorKind::Other)?;
self.f.write_str(s).map_err(|_| io::ErrorKind::Other)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'s> CliReport<'s> {
pub fn builder(
sources: &'s SourceMap,
main_location: Option<SourceSpan>,
color: Color,
) -> Self {
let span = main_location
.and_then(to_span)
.unwrap_or((FileId::NONE, 0..0));
let report = ariadne::Report::build(ReportKind::Error, span);
let enable_color = match color {
Color::Never => false,
Color::StderrIsTerminal => true,
};
let config = ariadne::Config::new()
.with_index_type(ariadne::IndexType::Byte)
.with_color(enable_color);
Self {
sources,
colors: ColorGenerator::new(),
report: report.with_config(config),
}
}
pub fn with_message(&mut self, message: impl ToString) {
self.report.set_message(message);
}
pub fn with_help(&mut self, help: impl ToString) {
self.report.set_help(help);
}
pub fn with_note(&mut self, note: impl ToString) {
self.report.set_note(note);
}
pub fn with_label_opt(&mut self, location: Option<SourceSpan>, message: impl ToString) {
if let Some(span) = location.and_then(to_span) {
self.report.add_label(
ariadne::Label::new(span)
.with_message(message)
.with_color(self.colors.next()),
);
}
}
pub fn write(self, w: impl std::io::Write) -> std::io::Result<()> {
let report = self.report.finish();
report.write(Cache(self.sources), w)
}
pub fn fmt(self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write(WriteToFormatter { f }).map_err(|_| fmt::Error)
}
pub fn into_string(self) -> String {
struct OneTimeDisplay<'s>(Cell<Option<CliReport<'s>>>);
impl fmt::Display for OneTimeDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.take().unwrap().fmt(f)
}
}
OneTimeDisplay(Cell::new(Some(self))).to_string()
}
}
struct Cache<'a>(&'a SourceMap);
impl ariadne::Cache<FileId> for Cache<'_> {
type Storage = String;
fn fetch(&mut self, file_id: &FileId) -> Result<&ariadne::Source, impl fmt::Debug> {
struct NotFound(FileId);
impl fmt::Debug for NotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "source file not found: {:?}", self.0)
}
}
if let Some(source_file) = self.0.get(file_id) {
Ok(source_file.ariadne())
} else if *file_id == FileId::NONE {
static EMPTY: OnceLock<ariadne::Source> = OnceLock::new();
Ok(EMPTY.get_or_init(|| ariadne::Source::from(String::new())))
} else {
Err(NotFound(*file_id))
}
}
fn display<'a>(&self, file_id: &'a FileId) -> Option<impl fmt::Display + 'a> {
enum Path {
SourceFile(Arc<SourceFile>),
NoSourceFile,
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Path::SourceFile(source_file) => source_file.path().display().fmt(f),
Path::NoSourceFile => f.write_str("(no source file)"),
}
}
}
if *file_id != FileId::NONE {
let source_file = self.0.get(file_id)?;
Some(Path::SourceFile(source_file.clone()))
} else {
Some(Path::NoSourceFile)
}
}
}
impl<T: ToCliReport> std::error::Error for Diagnostic<'_, T> {}
impl<T: ToCliReport> Diagnostic<'_, T> {
pub fn line_column_range(&self) -> Option<Range<LineColumn>> {
self.error.location()?.line_column_range(self.sources)
}
pub fn to_json(&self) -> GraphQLError
where
T: ToString,
{
GraphQLError::new(self.error.to_string(), self.error.location(), self.sources)
}
pub fn to_report(&self, color: Color) -> CliReport<'_> {
self.error.to_report(self.sources, color)
}
}
impl<T: ToCliReport> fmt::Debug for Diagnostic<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.to_report(Color::StderrIsTerminal).fmt(f)
}
}
impl<T: ToCliReport> fmt::Display for Diagnostic<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.to_report(Color::Never).fmt(f)
}
}