use crate::ByteSpan;
use crate::GraphQLErrorNote;
use crate::GraphQLErrorNoteKind;
use crate::GraphQLParseError;
use crate::GraphQLParseErrorKind;
use crate::ReservedNameContext;
use crate::smallvec::SmallVec;
use crate::SourceMap;
use crate::SourcePosition;
use crate::SourceSpan;
fn unexpected_token_kind() -> GraphQLParseErrorKind {
GraphQLParseErrorKind::UnexpectedToken {
expected: vec![":".to_string()],
found: "String".to_string(),
}
}
fn unclosed_delimiter_kind() -> GraphQLParseErrorKind {
GraphQLParseErrorKind::UnclosedDelimiter {
delimiter: "{".to_string(),
}
}
#[test]
fn parse_error_new_creates_empty_notes() {
let error = GraphQLParseError::new(
"Expected `:`",
unexpected_token_kind(),
SourceSpan::zero(),
);
assert_eq!(error.message(), "Expected `:`");
assert!(matches!(
error.kind(),
GraphQLParseErrorKind::UnexpectedToken { .. }
));
assert!(error.notes().is_empty());
}
#[test]
fn parse_error_with_notes_constructor() {
let mut notes = SmallVec::new();
notes.push(GraphQLErrorNote::general("Additional context"));
notes.push(GraphQLErrorNote::help("Try adding a colon here"));
let error = GraphQLParseError::with_notes(
"Expected `:`",
unexpected_token_kind(),
notes,
SourceSpan::zero(),
);
assert_eq!(error.message(), "Expected `:`");
assert_eq!(error.notes().len(), 2);
}
#[test]
fn parse_error_from_lexer_error() {
let mut lexer_notes = SmallVec::new();
lexer_notes.push(GraphQLErrorNote::general(
"Lexer detected unterminated string",
));
let error = GraphQLParseError::from_lexer_error(
"Unterminated string",
lexer_notes,
SourceSpan::zero(),
);
assert_eq!(error.message(), "Unterminated string");
assert!(matches!(
error.kind(),
GraphQLParseErrorKind::LexerError
));
assert_eq!(error.notes().len(), 1);
}
#[test]
fn parse_error_add_note() {
let mut error = GraphQLParseError::new(
"Primary error",
unexpected_token_kind(),
SourceSpan::zero(),
);
error.add_note("This is additional context");
assert_eq!(error.notes().len(), 1);
let note = &error.notes()[0];
assert!(matches!(note.kind, GraphQLErrorNoteKind::General));
assert_eq!(note.message, "This is additional context");
assert!(note.span.is_none());
}
#[test]
fn parse_error_add_note_with_span() {
let mut error = GraphQLParseError::new(
"Primary error",
unclosed_delimiter_kind(),
SourceSpan::zero(),
);
error.add_note_with_span("Opening `{` here", SourceSpan::zero());
assert_eq!(error.notes().len(), 1);
let note = &error.notes()[0];
assert!(matches!(note.kind, GraphQLErrorNoteKind::General));
assert!(note.span.is_some());
}
#[test]
fn parse_error_add_help() {
let mut error = GraphQLParseError::new(
"Missing colon",
unexpected_token_kind(),
SourceSpan::zero(),
);
error.add_help("Did you mean: `fieldName: Type`?");
assert_eq!(error.notes().len(), 1);
let note = &error.notes()[0];
assert!(matches!(note.kind, GraphQLErrorNoteKind::Help));
assert_eq!(
note.message,
"Did you mean: `fieldName: Type`?",
);
assert!(note.span.is_none());
}
#[test]
fn parse_error_add_help_with_span() {
let mut error = GraphQLParseError::new(
"Unknown directive location",
GraphQLParseErrorKind::InvalidSyntax,
SourceSpan::zero(),
);
error.add_help_with_span(
"Did you mean `FIELD`?",
SourceSpan::zero(),
);
assert_eq!(error.notes().len(), 1);
let note = &error.notes()[0];
assert!(matches!(note.kind, GraphQLErrorNoteKind::Help));
assert!(note.span.is_some());
}
#[test]
fn parse_error_add_spec() {
let mut error = GraphQLParseError::new(
"Invalid enum value",
GraphQLParseErrorKind::ReservedName {
name: "true".to_string(),
context: ReservedNameContext::EnumValue,
},
SourceSpan::zero(),
);
error.add_spec(
"https://spec.graphql.org/September2025/#sec-Enums",
);
assert_eq!(error.notes().len(), 1);
let note = &error.notes()[0];
assert!(matches!(note.kind, GraphQLErrorNoteKind::Spec));
}
#[test]
fn parse_error_multiple_notes() {
let mut error = GraphQLParseError::new(
"Unclosed brace",
unclosed_delimiter_kind(),
SourceSpan::zero(),
);
error.add_note(
"Expected `}` to close type definition",
);
error.add_note_with_span(
"Opening `{` here",
SourceSpan::zero(),
);
error.add_help(
"Add a closing `}` at the end of the type definition",
);
assert_eq!(error.notes().len(), 3);
}
#[test]
fn parse_error_format_oneline() {
let resolved = SourceSpan::new(
SourcePosition::new(4, 11, Some(11), 55),
SourcePosition::new(4, 16, Some(16), 60),
);
let error = GraphQLParseError::new(
"Expected `:` after field name",
unexpected_token_kind(),
resolved,
);
let formatted = error.format_oneline();
assert_eq!(
formatted,
"<input>:5:12: error: Expected `:` after field name",
);
}
#[test]
fn parse_error_format_detailed_without_source() {
let error = GraphQLParseError::new(
"Unexpected token",
unexpected_token_kind(),
SourceSpan::zero(),
);
let formatted = error.format_detailed(None);
assert!(formatted.contains("error:"));
assert!(formatted.contains("Unexpected token"));
assert!(formatted.contains("-->"));
assert!(formatted.contains("1:1"));
}
#[test]
fn parse_error_format_detailed_with_source() {
let source =
"type Query {\n userName String\n}";
let sm = SourceMap::new_with_source(source, None);
let resolved = sm
.resolve_span(ByteSpan::new(26, 32))
.unwrap_or_else(SourceSpan::zero);
let error = GraphQLParseError::new(
"Expected `:` after field name",
unexpected_token_kind(),
resolved,
);
let formatted = error.format_detailed(Some(source));
assert!(formatted.contains("error:"));
assert!(formatted.contains(
"Expected `:` after field name",
));
assert!(formatted.contains("userName String"));
}
#[test]
fn parse_error_format_detailed_with_notes() {
let mut error = GraphQLParseError::new(
"Unclosed `{`",
unclosed_delimiter_kind(),
SourceSpan::zero(),
);
error.add_note(
"Expected `}` to close type definition",
);
error.add_help(
"Check that all braces are properly matched",
);
let formatted = error.format_detailed(None);
assert!(formatted.contains("= note:"));
assert!(formatted.contains(
"Expected `}` to close type definition",
));
assert!(formatted.contains("= help:"));
assert!(formatted.contains(
"Check that all braces are properly matched",
));
}
#[test]
fn parse_error_format_detailed_with_spec_note() {
let mut error = GraphQLParseError::new(
"Invalid enum value name",
GraphQLParseErrorKind::ReservedName {
name: "null".to_string(),
context: ReservedNameContext::EnumValue,
},
SourceSpan::zero(),
);
error.add_spec(
"https://spec.graphql.org/September2025/#sec-Enums",
);
let formatted = error.format_detailed(None);
assert!(formatted.contains("= spec:"));
assert!(formatted.contains("spec.graphql.org"));
}
#[test]
fn parse_error_message_accessor() {
let error = GraphQLParseError::new(
"Test message",
unexpected_token_kind(),
SourceSpan::zero(),
);
assert_eq!(error.message(), "Test message");
}
#[test]
fn parse_error_source_span_accessor() {
let span = SourceSpan::new(
SourcePosition::new(0, 0, None, 20),
SourcePosition::new(0, 5, None, 25),
);
let error = GraphQLParseError::new(
"Error",
unexpected_token_kind(),
span,
);
assert_eq!(
error.source_span().start_inclusive.byte_offset(),
20,
);
assert_eq!(
error.source_span().end_exclusive.byte_offset(),
25,
);
}
#[test]
fn parse_error_kind_accessor() {
let error = GraphQLParseError::new(
"Error",
unclosed_delimiter_kind(),
SourceSpan::zero(),
);
assert!(matches!(
error.kind(),
GraphQLParseErrorKind::UnclosedDelimiter { .. }
));
}
#[test]
fn parse_error_notes_accessor() {
let mut notes = SmallVec::new();
notes.push(GraphQLErrorNote::general("note 1"));
notes.push(GraphQLErrorNote::help("note 2"));
let error = GraphQLParseError::with_notes(
"Error",
unexpected_token_kind(),
notes,
SourceSpan::zero(),
);
assert_eq!(error.notes().len(), 2);
}
#[test]
fn parse_error_format_detailed_with_bare_cr_line_endings() {
let source = "type Query {\r hello: String\r}";
let sm = SourceMap::new_with_source(source, None);
let resolved = sm
.resolve_span(ByteSpan::new(15, 20))
.unwrap_or_else(SourceSpan::zero);
let error = GraphQLParseError::new(
"test error on CR-only source",
unexpected_token_kind(),
resolved,
);
let formatted = error.format_detailed(Some(source));
assert!(
formatted.contains(" 2 |"),
"Snippet should show line number 2 for the \
\\r-separated line containing 'hello', but \
got:\n{formatted}",
);
assert!(
formatted.contains("^^^^^"),
"Snippet should underline 'hello' with 5 carets, \
but got:\n{formatted}",
);
}
#[test]
fn parse_error_format_note_snippet_with_bare_cr_line_endings() {
let source = "type Query {\r hello: String\r}";
let sm = SourceMap::new_with_source(source, None);
let resolved = sm
.resolve_span(ByteSpan::new(0, 1))
.unwrap_or_else(SourceSpan::zero);
let mut error = GraphQLParseError::new(
"primary error",
unexpected_token_kind(),
resolved,
);
let note_span = sm
.resolve_span(ByteSpan::new(15, 20))
.unwrap_or_else(SourceSpan::zero);
error.add_note_with_span("see this token", note_span);
let formatted = error.format_detailed(Some(source));
assert!(
formatted.contains(" 2 |"),
"Note snippet should show line number 2 for the \
\\r-separated line containing 'hello', but \
got:\n{formatted}",
);
}
#[test]
fn parse_error_display_trait_without_source() {
let error = GraphQLParseError::new(
"Test error message",
unexpected_token_kind(),
SourceSpan::zero(),
);
let display_output = format!("{error}");
assert_eq!(
display_output,
"<input>:1:1: error: Test error message",
);
}
#[test]
fn parse_error_display_trait_with_resolved_span() {
use std::path::PathBuf;
let resolved = SourceSpan::with_file(
SourcePosition::new(
4, 11,
Some(11), 55,
),
SourcePosition::new(
4, 16,
Some(16), 60,
),
PathBuf::from("schema.graphql"),
);
let error = GraphQLParseError::new(
"Expected `:` after field name",
unexpected_token_kind(),
resolved,
);
let display_output = format!("{error}");
assert_eq!(
display_output,
"schema.graphql:5:12: error: Expected `:` after \
field name",
);
}
#[test]
fn parse_error_display_trait_resolved_span_no_file() {
let resolved = SourceSpan::new(
SourcePosition::new(2, 5, Some(5), 30),
SourcePosition::new(2, 10, Some(10), 35),
);
let error = GraphQLParseError::new(
"Unexpected token",
unexpected_token_kind(),
resolved,
);
let display_output = format!("{error}");
assert_eq!(
display_output,
"<input>:3:6: error: Unexpected token",
);
}
#[test]
fn parse_error_from_parser_has_resolved_span() {
use crate::GraphQLParser;
let source = "type Query {\n name String\n}";
let parser = GraphQLParser::new(source);
let result = parser.parse_schema_document();
assert!(
result.has_errors(),
"should have parse errors",
);
let error = &result.errors()[0];
let display = format!("{error}");
assert!(
display.contains(":2:"),
"Display should show real line number, got: \
{display}",
);
assert!(
!display.contains(":1:1: error:"),
"Display should not show fallback 1:1 position, \
got: {display}",
);
}