use crate::rust_macro_graphql_token_source::RustMacroGraphQLTokenSource;
use libgraphql_parser::token::GraphQLToken;
use libgraphql_parser::token::GraphQLTokenKind;
use libgraphql_parser::token::GraphQLTriviaToken;
use proc_macro2::TokenStream;
use quote::quote;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::str::FromStr;
fn tokenize(input: TokenStream) -> Vec<GraphQLTokenKind<'static>> {
let span_map = Rc::new(RefCell::new(HashMap::new()));
let source =
RustMacroGraphQLTokenSource::new(input, span_map);
source.map(|t| t.kind).collect()
}
fn tokenize_full(input: TokenStream) -> Vec<GraphQLToken<'static>> {
let span_map = Rc::new(RefCell::new(HashMap::new()));
let source =
RustMacroGraphQLTokenSource::new(input, span_map);
source.collect()
}
fn tokenize_str(input: &str) -> Vec<GraphQLTokenKind<'static>> {
let stream = TokenStream::from_str(input)
.expect("Failed to parse as Rust tokens");
let span_map = Rc::new(RefCell::new(HashMap::new()));
let source =
RustMacroGraphQLTokenSource::new(stream, span_map);
source.map(|t| t.kind).collect()
}
fn tokenize_str_full_with_span_map(
input: &str,
) -> (Vec<GraphQLToken<'static>>, HashMap<u32, proc_macro2::Span>) {
let stream = TokenStream::from_str(input)
.expect("Failed to parse as Rust tokens");
let span_map = Rc::new(RefCell::new(HashMap::new()));
let source =
RustMacroGraphQLTokenSource::new(stream, span_map.clone());
let tokens: Vec<_> = source.collect();
let map = Rc::try_unwrap(span_map)
.expect("span_map Rc should have one ref")
.into_inner();
(tokens, map)
}
#[test]
fn test_simple_type_definition() {
let kinds = tokenize(quote! { type Query { name: String } });
assert_eq!(kinds.len(), 8, "Expected 8 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "type"));
assert!(matches!(&kinds[1], GraphQLTokenKind::Name(n) if n == "Query"));
assert!(matches!(&kinds[2], GraphQLTokenKind::CurlyBraceOpen));
assert!(matches!(&kinds[3], GraphQLTokenKind::Name(n) if n == "name"));
assert!(matches!(&kinds[4], GraphQLTokenKind::Colon));
assert!(matches!(&kinds[5], GraphQLTokenKind::Name(n) if n == "String"));
assert!(matches!(&kinds[6], GraphQLTokenKind::CurlyBraceClose));
assert!(matches!(&kinds[7], GraphQLTokenKind::Eof));
}
#[test]
fn test_commas_as_trivia() {
let tokens = tokenize_full(quote! { a, b, c });
assert_eq!(tokens.len(), 4, "Expected 4 tokens (commas are trivia)");
assert!(tokens[0].preceding_trivia.is_empty());
assert_eq!(
tokens[1].preceding_trivia.len(),
1,
"Second token should have comma trivia"
);
assert!(matches!(
&tokens[1].preceding_trivia[0],
GraphQLTriviaToken::Comma { .. }
));
assert_eq!(
tokens[2].preceding_trivia.len(),
1,
"Third token should have comma trivia"
);
assert!(matches!(
&tokens[2].preceding_trivia[0],
GraphQLTriviaToken::Comma { .. }
));
}
#[test]
fn test_integer_literals() {
let kinds = tokenize(quote! { 0 42 1000000 });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::IntValue(v) if v == "0"));
assert!(matches!(&kinds[1], GraphQLTokenKind::IntValue(v) if v == "42"));
assert!(matches!(&kinds[2], GraphQLTokenKind::IntValue(v) if v == "1000000"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_negative_integer_literals() {
let kinds = tokenize(quote! { 42 -17 -1 });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::IntValue(v) if v == "42"));
assert!(matches!(&kinds[1], GraphQLTokenKind::IntValue(v) if v == "-17"));
assert!(matches!(&kinds[2], GraphQLTokenKind::IntValue(v) if v == "-1"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_float_literals() {
let kinds = tokenize(quote! { 3.14 0.5 1e10 2.5e-3 });
assert_eq!(kinds.len(), 5, "Expected 5 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::FloatValue(v) if v == "3.14"));
assert!(matches!(&kinds[1], GraphQLTokenKind::FloatValue(v) if v == "0.5"));
assert!(matches!(&kinds[2], GraphQLTokenKind::FloatValue(v) if v == "1e10"));
assert!(matches!(&kinds[3], GraphQLTokenKind::FloatValue(v) if v == "2.5e-3"));
assert!(matches!(&kinds[4], GraphQLTokenKind::Eof));
}
#[test]
fn test_negative_float_literals() {
let kinds = tokenize(quote! { 3.14 -2.718 -1e5 });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::FloatValue(v) if v == "3.14"));
assert!(matches!(&kinds[1], GraphQLTokenKind::FloatValue(v) if v == "-2.718"));
assert!(matches!(&kinds[2], GraphQLTokenKind::FloatValue(v) if v == "-1e5"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_string_literals() {
let kinds = tokenize(quote! { "hello" "with\nescape" });
assert_eq!(kinds.len(), 3, "Expected 3 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::StringValue(v) if v == "\"hello\""));
assert!(matches!(&kinds[1], GraphQLTokenKind::StringValue(v) if v == "\"with\\nescape\""));
assert!(matches!(&kinds[2], GraphQLTokenKind::Eof));
}
#[test]
fn test_boolean_literals() {
let kinds = tokenize(quote! { true false });
assert_eq!(kinds.len(), 3, "Expected 3 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::True));
assert!(matches!(&kinds[1], GraphQLTokenKind::False));
assert!(matches!(&kinds[2], GraphQLTokenKind::Eof));
}
#[test]
fn test_null_literal() {
let kinds = tokenize(quote! { null });
assert_eq!(kinds.len(), 2, "Expected 2 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Null));
assert!(matches!(&kinds[1], GraphQLTokenKind::Eof));
}
#[test]
fn test_punctuators() {
let kinds = tokenize(quote! { ! $ & ( ) : = @ [ ] { | } });
assert_eq!(kinds.len(), 14, "Expected 14 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Bang));
assert!(matches!(&kinds[1], GraphQLTokenKind::Dollar));
assert!(matches!(&kinds[2], GraphQLTokenKind::Ampersand));
assert!(matches!(&kinds[3], GraphQLTokenKind::ParenOpen));
assert!(matches!(&kinds[4], GraphQLTokenKind::ParenClose));
assert!(matches!(&kinds[5], GraphQLTokenKind::Colon));
assert!(matches!(&kinds[6], GraphQLTokenKind::Equals));
assert!(matches!(&kinds[7], GraphQLTokenKind::At));
assert!(matches!(&kinds[8], GraphQLTokenKind::SquareBracketOpen));
assert!(matches!(&kinds[9], GraphQLTokenKind::SquareBracketClose));
assert!(matches!(&kinds[10], GraphQLTokenKind::CurlyBraceOpen));
assert!(matches!(&kinds[11], GraphQLTokenKind::Pipe));
assert!(matches!(&kinds[12], GraphQLTokenKind::CurlyBraceClose));
assert!(matches!(&kinds[13], GraphQLTokenKind::Eof));
}
#[test]
fn test_ellipsis() {
let kinds = tokenize(quote! { ...Fragment });
assert_eq!(kinds.len(), 3, "Expected 3 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Ellipsis));
assert!(matches!(&kinds[1], GraphQLTokenKind::Name(n) if n == "Fragment"));
assert!(matches!(&kinds[2], GraphQLTokenKind::Eof));
}
#[test]
fn test_spaced_dots_produce_error() {
let kinds = tokenize_str(". .");
assert_eq!(kinds.len(), 2, "Expected 2 tokens including Eof");
assert!(matches!(
&kinds[0],
GraphQLTokenKind::Error(err)
if err.message.contains("`. .`")
));
assert!(matches!(&kinds[1], GraphQLTokenKind::Eof));
}
#[test]
fn test_single_dot_produces_error() {
let kinds = tokenize(quote! { name . field });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "name"));
assert!(matches!(
&kinds[1],
GraphQLTokenKind::Error(err)
if err.message.contains("`.`")
));
assert!(matches!(&kinds[2], GraphQLTokenKind::Name(n) if n == "field"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_double_dot_produces_error() {
let kinds = tokenize(quote! { name..field });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "name"));
assert!(matches!(
&kinds[1],
GraphQLTokenKind::Error(err)
if err.message.contains("`..`")
));
assert!(matches!(&kinds[2], GraphQLTokenKind::Name(n) if n == "field"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_raw_string_produces_error() {
let kinds = tokenize(quote! { r"raw content" });
assert_eq!(kinds.len(), 2, "Expected 2 tokens including Eof");
assert!(matches!(
&kinds[0],
GraphQLTokenKind::Error(err)
if err.message.contains("raw string")
&& err.error_notes.len() == 1
&& err.error_notes[0].message.contains("Consider using:")
));
assert!(matches!(&kinds[1], GraphQLTokenKind::Eof));
}
#[test]
fn test_standalone_minus_produces_error() {
let kinds = tokenize(quote! { a - b });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "a"));
assert!(matches!(
&kinds[1],
GraphQLTokenKind::Error(err)
if err.message.contains("`-`")
));
assert!(matches!(&kinds[2], GraphQLTokenKind::Name(n) if n == "b"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_unexpected_punct_produces_error() {
let kinds = tokenize(quote! { a % b });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "a"));
assert!(matches!(
&kinds[1],
GraphQLTokenKind::Error(err)
if err.message.contains("`%`")
));
assert!(matches!(&kinds[2], GraphQLTokenKind::Name(n) if n == "b"));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_complete_query() {
let kinds = tokenize(quote! { query GetUser($id: ID!) { user(id: $id) { name } } });
assert_eq!(kinds.len(), 22, "Expected 22 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "query"));
assert!(matches!(&kinds[1], GraphQLTokenKind::Name(n) if n == "GetUser"));
assert!(matches!(&kinds[2], GraphQLTokenKind::ParenOpen));
assert!(matches!(&kinds[3], GraphQLTokenKind::Dollar));
assert!(matches!(&kinds[4], GraphQLTokenKind::Name(n) if n == "id"));
assert!(matches!(&kinds[6], GraphQLTokenKind::Name(n) if n == "ID"));
assert!(matches!(&kinds[7], GraphQLTokenKind::Bang));
assert!(matches!(&kinds[21], GraphQLTokenKind::Eof));
}
#[test]
fn test_block_string() {
let kinds = tokenize_str(r#""""block content""""#);
assert_eq!(kinds.len(), 2, "Expected 2 tokens including Eof");
assert!(matches!(
&kinds[0],
GraphQLTokenKind::StringValue(v) if v == "\"\"\"block content\"\"\""
));
assert!(matches!(&kinds[1], GraphQLTokenKind::Eof));
}
#[test]
fn test_spaced_quotes_not_block_string() {
let kinds = tokenize(quote! { "" "content" "" });
assert_eq!(kinds.len(), 4, "Expected 4 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::StringValue(v) if v == "\"\""));
assert!(matches!(&kinds[1], GraphQLTokenKind::StringValue(v) if v == "\"content\""));
assert!(matches!(&kinds[2], GraphQLTokenKind::StringValue(v) if v == "\"\""));
assert!(matches!(&kinds[3], GraphQLTokenKind::Eof));
}
#[test]
fn test_trailing_comma_attached_to_eof() {
let tokens = tokenize_full(quote! { a, });
assert_eq!(tokens.len(), 2, "Expected 2 tokens including Eof");
assert!(matches!(&tokens[1].kind, GraphQLTokenKind::Eof));
assert_eq!(
tokens[1].preceding_trivia.len(),
1,
"Eof should have trailing comma as trivia"
);
assert!(matches!(
&tokens[1].preceding_trivia[0],
GraphQLTriviaToken::Comma { .. }
));
}
#[test]
fn test_multiple_consecutive_commas_as_trivia() {
let tokens = tokenize_full(quote! { a,,, b });
assert_eq!(
tokens.len(),
3,
"Expected 3 tokens (commas are trivia)",
);
assert!(matches!(
&tokens[0].kind,
GraphQLTokenKind::Name(n) if n == "a",
));
assert!(tokens[0].preceding_trivia.is_empty());
assert!(matches!(
&tokens[1].kind,
GraphQLTokenKind::Name(n) if n == "b",
));
assert_eq!(
tokens[1].preceding_trivia.len(),
3,
"Second token should have 3 comma trivia items",
);
for trivia in &tokens[1].preceding_trivia {
assert!(
matches!(trivia, GraphQLTriviaToken::Comma { .. }),
"All trivia should be Comma, got: {trivia:?}",
);
}
assert!(matches!(&tokens[2].kind, GraphQLTokenKind::Eof));
assert!(tokens[2].preceding_trivia.is_empty());
}
#[test]
fn test_multiple_trailing_commas_on_eof() {
let tokens = tokenize_full(quote! { a,,, });
assert_eq!(
tokens.len(),
2,
"Expected 2 tokens (commas are trivia)",
);
assert!(matches!(
&tokens[0].kind,
GraphQLTokenKind::Name(n) if n == "a",
));
assert!(tokens[0].preceding_trivia.is_empty());
assert!(matches!(&tokens[1].kind, GraphQLTokenKind::Eof));
assert_eq!(
tokens[1].preceding_trivia.len(),
3,
"Eof should have 3 trailing commas as trivia",
);
for trivia in &tokens[1].preceding_trivia {
assert!(
matches!(trivia, GraphQLTriviaToken::Comma { .. }),
"All trivia should be Comma, got: {trivia:?}",
);
}
}
#[test]
fn test_comma_trivia_span_positions() {
let (tokens, span_map) =
tokenize_str_full_with_span_map("a, b");
assert_eq!(
tokens.len(),
3,
"Expected 3 tokens (a, b, Eof)",
);
assert!(matches!(
&tokens[0].kind,
GraphQLTokenKind::Name(n) if n == "a",
));
let a_span = span_map.get(&tokens[0].span.start)
.expect("span_map should have entry for `a`");
assert_eq!(a_span.start().column, 0);
assert!(matches!(
&tokens[1].kind,
GraphQLTokenKind::Name(n) if n == "b",
));
let b_span = span_map.get(&tokens[1].span.start)
.expect("span_map should have entry for `b`");
assert_eq!(b_span.start().column, 3);
assert_eq!(tokens[1].preceding_trivia.len(), 1);
if let GraphQLTriviaToken::Comma { span } =
&tokens[1].preceding_trivia[0]
{
let comma_span = span_map.get(&span.start)
.expect("span_map should have entry for comma");
assert_eq!(
comma_span.start().column,
1,
"Comma trivia should be at column 1",
);
} else {
panic!("Expected Comma trivia");
}
}
#[test]
fn test_comma_trivia_across_delimiter_groups() {
let tokens = tokenize_full(quote! {
query {
users(limit: 10, offset: 0) {
name,
email,
}
}
});
let name_tokens: Vec<_> = tokens
.iter()
.filter(|t| matches!(&t.kind, GraphQLTokenKind::Name(_)))
.collect();
let offset_token = name_tokens
.iter()
.find(|t| matches!(
&t.kind,
GraphQLTokenKind::Name(n) if n == "offset",
))
.expect("Should have 'offset' token");
assert_eq!(
offset_token.preceding_trivia.len(),
1,
"'offset' should have 1 comma trivia",
);
assert!(matches!(
&offset_token.preceding_trivia[0],
GraphQLTriviaToken::Comma { .. },
));
let email_token = name_tokens
.iter()
.find(|t| matches!(
&t.kind,
GraphQLTokenKind::Name(n) if n == "email",
))
.expect("Should have 'email' token");
assert_eq!(
email_token.preceding_trivia.len(),
1,
"'email' should have 1 comma trivia",
);
assert!(matches!(
&email_token.preceding_trivia[0],
GraphQLTriviaToken::Comma { .. },
));
}
#[test]
fn test_empty_input() {
let kinds = tokenize(quote! {});
assert_eq!(kinds.len(), 1, "Expected 1 token (Eof)");
assert!(matches!(&kinds[0], GraphQLTokenKind::Eof));
}
#[test]
fn test_position_tracking() {
let (tokens, span_map) =
tokenize_str_full_with_span_map("type Query");
assert_eq!(tokens.len(), 3, "Expected 3 tokens including Eof");
let type_token = &tokens[0];
let query_token = &tokens[1];
let type_span = span_map.get(&type_token.span.start)
.expect("span_map should have entry for `type`");
let query_span = span_map.get(&query_token.span.start)
.expect("span_map should have entry for `Query`");
assert_eq!(type_span.start().line, 1);
assert_eq!(query_span.start().line, 1);
assert_eq!(type_span.start().column, 0);
assert_eq!(query_span.start().column, 5);
}
#[test]
fn test_directive_with_arguments() {
let kinds = tokenize(quote! { @deprecated(reason: "old") });
assert_eq!(kinds.len(), 8, "Expected 8 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::At));
assert!(matches!(&kinds[1], GraphQLTokenKind::Name(n) if n == "deprecated"));
assert!(matches!(&kinds[2], GraphQLTokenKind::ParenOpen));
assert!(matches!(&kinds[3], GraphQLTokenKind::Name(n) if n == "reason"));
assert!(matches!(&kinds[4], GraphQLTokenKind::Colon));
assert!(matches!(&kinds[5], GraphQLTokenKind::StringValue(v) if v == "\"old\""));
assert!(matches!(&kinds[6], GraphQLTokenKind::ParenClose));
assert!(matches!(&kinds[7], GraphQLTokenKind::Eof));
}
#[test]
fn test_array_values() {
let kinds = tokenize(quote! { [1, 2, 3] });
assert_eq!(kinds.len(), 6, "Expected 6 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::SquareBracketOpen));
assert!(matches!(&kinds[1], GraphQLTokenKind::IntValue(v) if v == "1"));
assert!(matches!(&kinds[2], GraphQLTokenKind::IntValue(v) if v == "2"));
assert!(matches!(&kinds[3], GraphQLTokenKind::IntValue(v) if v == "3"));
assert!(matches!(&kinds[4], GraphQLTokenKind::SquareBracketClose));
assert!(matches!(&kinds[5], GraphQLTokenKind::Eof));
}
#[test]
fn test_union_type() {
let kinds = tokenize(quote! { union SearchResult = User | Post | Comment });
assert_eq!(kinds.len(), 9, "Expected 9 tokens including Eof");
assert!(matches!(&kinds[0], GraphQLTokenKind::Name(n) if n == "union"));
assert!(matches!(&kinds[1], GraphQLTokenKind::Name(n) if n == "SearchResult"));
assert!(matches!(&kinds[2], GraphQLTokenKind::Equals));
assert!(matches!(&kinds[3], GraphQLTokenKind::Name(n) if n == "User"));
assert!(matches!(&kinds[4], GraphQLTokenKind::Pipe));
assert!(matches!(&kinds[5], GraphQLTokenKind::Name(n) if n == "Post"));
assert!(matches!(&kinds[6], GraphQLTokenKind::Pipe));
assert!(matches!(&kinds[7], GraphQLTokenKind::Name(n) if n == "Comment"));
assert!(matches!(&kinds[8], GraphQLTokenKind::Eof));
}