use crate::parse_error_converter::convert_parse_errors_to_tokenstream;
use crate::parse_error_converter::format_parse_error_message;
use crate::parse_error_converter::format_parse_error_note;
use crate::span_map::SpanMap;
use libgraphql_parser::GraphQLErrorNote;
use libgraphql_parser::GraphQLParseError;
use libgraphql_parser::GraphQLParseErrorKind;
use libgraphql_parser::SourcePosition;
use libgraphql_parser::SourceSpan;
use proc_macro2::Span;
use std::collections::HashMap;
fn error_at(message: &str, offset: u32) -> GraphQLParseError {
GraphQLParseError::new(
message.to_string(),
GraphQLParseErrorKind::InvalidSyntax,
SourceSpan::new(
SourcePosition::new(0, 0, None, offset as usize),
SourcePosition::new(
0, 0, None, (offset + 1) as usize,
),
),
)
}
fn simple_error(message: &str) -> GraphQLParseError {
error_at(message, 0)
}
fn count_compile_errors(stream_str: &str) -> usize {
stream_str.matches("compile_error").count()
}
#[test]
fn format_message_no_notes() {
let error = simple_error("unexpected token `{`");
let result = format_parse_error_message(&error);
assert_eq!(result, "unexpected token `{`");
}
#[test]
fn format_message_with_general_note() {
let mut error = simple_error("unexpected token `{`");
error.add_note("expected a field name");
let result = format_parse_error_message(&error);
assert_eq!(
result,
"unexpected token `{`\n = note: expected a field name",
);
}
#[test]
fn format_message_with_help_note() {
let mut error = simple_error("unexpected token `{`");
error.add_help("try adding a field name before `{`");
let result = format_parse_error_message(&error);
assert_eq!(
result,
"unexpected token `{`\
\n = help: try adding a field name before `{`",
);
}
#[test]
fn format_message_with_spec_note() {
let mut error = simple_error("invalid directive location");
error.add_spec(
"https://spec.graphql.org/September2025/#sec-Type-System\
.Directives",
);
let result = format_parse_error_message(&error);
assert_eq!(
result,
"invalid directive location\
\n = spec: https://spec.graphql.org/September2025/\
#sec-Type-System.Directives",
);
}
#[test]
fn format_message_with_mixed_notes() {
let mut error = simple_error("duplicate field `name`");
error.add_note("previous definition was here");
error.add_help("remove or rename one of the fields");
error.add_spec("https://spec.graphql.org/September2025/#Fields");
let result = format_parse_error_message(&error);
assert_eq!(
result,
"duplicate field `name`\
\n = note: previous definition was here\
\n = help: remove or rename one of the fields\
\n = spec: https://spec.graphql.org/September2025/\
#Fields",
);
}
#[test]
fn format_message_preserves_special_chars_in_note() {
let mut error = simple_error("parse error");
error.add_note("found `\"hello\"`");
let result = format_parse_error_message(&error);
assert!(
result.contains("note: found `\"hello\"`"),
"Note message should be included verbatim, got: {result}",
);
}
#[test]
fn format_note_general() {
let note = GraphQLErrorNote::general(
"previous definition was here".to_string(),
);
let result = format_parse_error_note(¬e);
assert_eq!(result, "note: previous definition was here");
}
#[test]
fn format_note_help() {
let note = GraphQLErrorNote::help(
"try removing the duplicate".to_string(),
);
let result = format_parse_error_note(¬e);
assert_eq!(result, "help: try removing the duplicate");
}
#[test]
fn format_note_spec() {
let note = GraphQLErrorNote::spec(
"https://spec.graphql.org/September2025/#sec-Names"
.to_string(),
);
let result = format_parse_error_note(¬e);
assert_eq!(
result,
"spec: https://spec.graphql.org/September2025/#sec-Names",
);
}
#[test]
fn convert_empty_errors_produces_empty_stream() {
let errors: &[GraphQLParseError] = &[];
let span_map = SpanMap::new(HashMap::new());
let result = convert_parse_errors_to_tokenstream(
errors, &span_map,
);
assert!(
result.is_empty(),
"Empty errors should produce empty token stream",
);
}
#[test]
fn convert_single_error_no_notes() {
let error = error_at("unexpected `}`", 0);
let mut map = HashMap::new();
map.insert(0, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
1,
"Expected exactly 1 compile_error!, got: {output}",
);
assert!(
output.contains("unexpected `}`"),
"compile_error! should contain the error message, \
got: {output}",
);
}
#[test]
fn convert_error_with_spanned_note_emits_secondary_compile_error() {
let mut error = error_at("duplicate field `name`", 0);
error.add_note_with_span(
"previous definition was here".to_string(),
SourceSpan::new(
SourcePosition::new(0, 0, None, 10),
SourcePosition::new(0, 0, None, 11),
),
);
let mut map = HashMap::new();
map.insert(0, Span::call_site());
map.insert(10, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
2,
"Expected 2 compile_error! invocations (primary + note), \
got: {output}",
);
assert!(
output.contains("previous definition was here"),
"Secondary compile_error! should contain the note message, \
got: {output}",
);
}
#[test]
fn convert_error_with_unspanned_note_no_extra_compile_error() {
let mut error = error_at("type mismatch", 0);
error.add_note("expected `String`, found `Int`");
let mut map = HashMap::new();
map.insert(0, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
1,
"Unspanned notes should NOT produce a secondary \
compile_error!, got: {output}",
);
assert!(
output.contains("expected `String`, found `Int`"),
"Note should appear inline in the primary message, \
got: {output}",
);
}
#[test]
fn convert_error_with_spanned_note_missing_from_map_is_skipped() {
let mut error = error_at("duplicate type", 0);
error.add_note_with_span(
"first defined here".to_string(),
SourceSpan::new(
SourcePosition::new(0, 0, None, 20),
SourcePosition::new(0, 0, None, 21),
),
);
let mut map = HashMap::new();
map.insert(0, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
1,
"Note with unmapped span should not produce a secondary \
compile_error!, got: {output}",
);
}
#[test]
fn convert_error_with_unmapped_primary_span_still_emits() {
let error = error_at("syntax error", 99);
let span_map = SpanMap::new(HashMap::new());
let result = convert_parse_errors_to_tokenstream(
&[error],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
1,
"Error should still emit compile_error! even when span \
lookup fails, got: {output}",
);
assert!(
output.contains("syntax error"),
"Message should be preserved even with fallback span, \
got: {output}",
);
}
#[test]
fn convert_multiple_errors() {
let error1 = error_at("first error", 0);
let error2 = error_at("second error", 2);
let error3 = error_at("third error", 4);
let mut map = HashMap::new();
map.insert(0, Span::call_site());
map.insert(2, Span::call_site());
map.insert(4, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error1, error2, error3],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
3,
"Each error should produce one compile_error!, \
got: {output}",
);
assert!(output.contains("first error"));
assert!(output.contains("second error"));
assert!(output.contains("third error"));
}
#[test]
fn convert_multiple_errors_with_mixed_notes() {
let mut error1 = error_at("error one", 0);
error1.add_note_with_span(
"note for error one".to_string(),
SourceSpan::new(
SourcePosition::new(0, 0, None, 6),
SourcePosition::new(0, 0, None, 7),
),
);
let mut error2 = error_at("error two", 2);
error2.add_help("some help");
let mut error3 = error_at("error three", 4);
error3.add_note_with_span(
"first note".to_string(),
SourceSpan::new(
SourcePosition::new(0, 0, None, 12),
SourcePosition::new(0, 0, None, 13),
),
);
error3.add_help_with_span(
"second note".to_string(),
SourceSpan::new(
SourcePosition::new(0, 0, None, 14),
SourcePosition::new(0, 0, None, 15),
),
);
let mut map = HashMap::new();
map.insert(0, Span::call_site());
map.insert(2, Span::call_site());
map.insert(4, Span::call_site());
map.insert(6, Span::call_site());
map.insert(12, Span::call_site());
map.insert(14, Span::call_site());
let span_map = SpanMap::new(map);
let result = convert_parse_errors_to_tokenstream(
&[error1, error2, error3],
&span_map,
);
let output = result.to_string();
assert_eq!(
count_compile_errors(&output),
6,
"Expected 6 compile_error! invocations (3 primary + 3 \
secondary from spanned notes), got: {output}",
);
}