use crate::GraphQLErrorNote;
use crate::GraphQLErrorNoteKind;
use crate::GraphQLParseErrorKind;
use crate::smallvec::SmallVec;
use crate::SourceSpan;
#[derive(Debug, Clone)]
pub struct GraphQLParseError {
message: String,
kind: GraphQLParseErrorKind,
notes: SmallVec<[GraphQLErrorNote; 2]>,
source_span: SourceSpan,
}
impl GraphQLParseError {
pub fn new(
message: impl Into<String>,
kind: GraphQLParseErrorKind,
source_span: SourceSpan,
) -> Self {
Self {
message: message.into(),
kind,
notes: SmallVec::new(),
source_span,
}
}
pub fn with_notes(
message: impl Into<String>,
kind: GraphQLParseErrorKind,
notes: SmallVec<[GraphQLErrorNote; 2]>,
source_span: SourceSpan,
) -> Self {
Self {
message: message.into(),
kind,
notes,
source_span,
}
}
pub fn from_lexer_error(
message: impl Into<String>,
lexer_notes: SmallVec<[GraphQLErrorNote; 2]>,
source_span: SourceSpan,
) -> Self {
Self {
message: message.into(),
kind: GraphQLParseErrorKind::LexerError,
notes: lexer_notes,
source_span,
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn source_span(&self) -> &SourceSpan {
&self.source_span
}
pub fn kind(&self) -> &GraphQLParseErrorKind {
&self.kind
}
pub fn notes(&self) -> &SmallVec<[GraphQLErrorNote; 2]> {
&self.notes
}
pub fn add_note(&mut self, message: impl Into<String>) {
self.notes.push(GraphQLErrorNote::general(message));
}
pub fn add_note_with_span(
&mut self,
message: impl Into<String>,
span: SourceSpan,
) {
self.notes.push(
GraphQLErrorNote::general_with_span(message, span),
);
}
pub fn add_help(&mut self, message: impl Into<String>) {
self.notes.push(GraphQLErrorNote::help(message));
}
pub fn add_help_with_span(
&mut self,
message: impl Into<String>,
span: SourceSpan,
) {
self.notes.push(
GraphQLErrorNote::help_with_span(message, span),
);
}
pub fn add_spec(&mut self, url: impl Into<String>) {
self.notes.push(GraphQLErrorNote::spec(url));
}
pub fn format_detailed(
&self,
source: Option<&str>,
) -> String {
let mut output = String::new();
output.push_str("error: ");
output.push_str(&self.message);
output.push('\n');
let file_name = self.source_span.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<input>".to_string());
let line =
self.source_span.start_inclusive.line() + 1;
let column =
self.source_span.start_inclusive.col_utf8() + 1;
output.push_str(
&format!(" --> {file_name}:{line}:{column}\n"),
);
if let Some(snippet) =
self.format_source_snippet(source)
{
output.push_str(&snippet);
}
for note in &self.notes {
let prefix = match note.kind {
GraphQLErrorNoteKind::General => "note",
GraphQLErrorNoteKind::Help => "help",
GraphQLErrorNoteKind::Spec => "spec",
};
output.push_str(
&format!(
" = {prefix}: {}\n",
note.message,
),
);
if let Some(note_span) = ¬e.span
&& let Some(snippet) =
Self::format_note_snippet(
source, note_span,
)
{
output.push_str(&snippet);
}
}
output
}
pub fn format_oneline(&self) -> String {
self.to_string()
}
fn format_source_snippet(
&self,
source: Option<&str>,
) -> Option<String> {
let source = source?;
let start_pos = &self.source_span.start_inclusive;
let end_pos = &self.source_span.end_exclusive;
let line_num = start_pos.line();
let line_content = get_line(source, line_num)?;
let display_line_num = line_num + 1;
let line_num_width =
display_line_num.to_string().len().max(2);
let mut output = String::new();
output.push_str(
&format!(
"{:>width$} |\n",
"",
width = line_num_width,
),
);
output.push_str(&format!(
"{display_line_num:>line_num_width$} | \
{line_content}\n"
));
let col_start = start_pos.col_utf8();
let col_end = end_pos.col_utf8();
let underline_len = if col_end > col_start {
col_end - col_start
} else {
1
};
output.push_str(&format!(
"{:>width$} | {:>padding$}{}\n",
"",
"",
"^".repeat(underline_len),
width = line_num_width,
padding = col_start
));
Some(output)
}
fn format_note_snippet(
source: Option<&str>,
span: &SourceSpan,
) -> Option<String> {
let source = source?;
let start_pos = &span.start_inclusive;
let line_num = start_pos.line();
let line_content = get_line(source, line_num)?;
let display_line_num = line_num + 1;
let line_num_width =
display_line_num.to_string().len().max(2);
let mut output = String::new();
output.push_str(&format!(
" {display_line_num:>line_num_width$} | \
{line_content}\n"
));
let col_start = start_pos.col_utf8();
output.push_str(&format!(
" {:>width$} | {:>padding$}-\n",
"",
"",
width = line_num_width,
padding = col_start
));
Some(output)
}
}
impl std::fmt::Display for GraphQLParseError {
fn fmt(
&self,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let file_name = self.source_span.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<input>".to_string());
let line =
self.source_span.start_inclusive.line() + 1;
let col =
self.source_span.start_inclusive.col_utf8() + 1;
write!(
f,
"{file_name}:{line}:{col}: error: {}",
self.message,
)
}
}
impl std::error::Error for GraphQLParseError {}
fn get_line(source: &str, line_index: usize) -> Option<&str> {
let bytes = source.as_bytes();
let mut current_line = 0;
let mut pos = 0;
while current_line < line_index {
match memchr::memchr2(b'\n', b'\r', &bytes[pos..]) {
Some(offset) => {
pos += offset;
if bytes[pos] == b'\r'
&& pos + 1 < bytes.len()
&& bytes[pos + 1] == b'\n'
{
pos += 2; } else {
pos += 1; }
current_line += 1;
},
None => return None, }
}
let line_start = pos;
let line_end = memchr::memchr2(b'\n', b'\r', &bytes[pos..])
.map(|offset| pos + offset)
.unwrap_or(bytes.len());
Some(&source[line_start..line_end])
}