oxc-graphql-parser 0.0.1

Spec-compliant GraphQL parser.
Documentation
use crate::parser::grammar::description;
use crate::parser::grammar::directive;
use crate::parser::grammar::name;
use crate::parser::grammar::selection;
use crate::parser::grammar::ty;
use crate::parser::grammar::value::Constness;
use crate::parser::grammar::variable;
use crate::Parser;
use crate::SyntaxKind;
use crate::TokenKind;
use crate::S;
use crate::T;

/// See: https://spec.graphql.org/draft/#FragmentDefinition
///
/// *FragmentDefinition*:
///     Description? **fragment** FragmentName VariableDefinitions? TypeCondition Directives? SelectionSet
///
/// The leading *Description* is only parsed when
/// [`Parser::allow_executable_descriptions`] is enabled (2025 draft spec,
/// accepted by graphql-js 16).
///
/// *VariableDefinitions* on a fragment are the "legacy fragment variables"
/// syntax, only parsed when [`Parser::allow_legacy_fragment_variables`] is
/// enabled (accepted by graphql-js 16's `allowLegacyFragmentVariables`).
pub(crate) fn fragment_definition(p: &mut Parser) {
    let _g = p.start_node(SyntaxKind::FRAGMENT_DEFINITION);

    if p.executable_descriptions_allowed() {
        if let Some(TokenKind::StringValue) = p.peek() {
            description::description(p);
        }
    }

    p.bump(SyntaxKind::fragment_KW);

    fragment_name(p);

    if p.legacy_fragment_variables_allowed() {
        if let Some(T!['(']) = p.peek() {
            variable::variable_definitions(p);
        }
    }

    type_condition(p);

    if let Some(T![@]) = p.peek() {
        directive::directives(p, Constness::NotConst);
    }

    match p.peek() {
        Some(T!['{']) => selection::selection_set(p),
        _ => p.err("expected a Selection Set"),
    }
}

/// See: https://spec.graphql.org/October2021/#FragmentName
///
/// *FragmentName*:
///     Name *but not* **on**
pub(crate) fn fragment_name(p: &mut Parser) {
    let _g = p.start_node(SyntaxKind::FRAGMENT_NAME);
    match p.peek_token() {
        Some(token) if token.kind() == TokenKind::Name && token.data() == "on" => {
            p.err("Fragment Name cannot be 'on'");
        }
        Some(token) if token.kind() == TokenKind::Name => {
            name::name(p);
        }
        _ => p.err("expected Fragment Name"),
    }
}

/// See: https://spec.graphql.org/October2021/#TypeCondition
///
/// *TypeCondition*:
///     **on** NamedType
pub(crate) fn type_condition(p: &mut Parser) {
    let _g = p.start_node(SyntaxKind::TYPE_CONDITION);
    if let Some(token) = p.peek_token() {
        if token.kind() == TokenKind::Name && token.data() == "on" {
            p.bump(SyntaxKind::on_KW);
        } else {
            p.err("expected 'on'");
        }

        if let Some(TokenKind::Name) = p.peek() {
            ty::named_type(p)
        } else {
            p.err("expected a Name in Type Condition")
        }
    } else {
        p.err("expected Type Condition")
    }
}

/// See: https://spec.graphql.org/October2021/#InlineFragment
///
/// *InlineFragment*:
///     **...** TypeCondition? Directives? SelectionSet
pub(crate) fn inline_fragment(p: &mut Parser) {
    let _g = p.start_node(SyntaxKind::INLINE_FRAGMENT);
    p.bump(S![...]);

    if let Some(TokenKind::Name) = p.peek() {
        type_condition(p);
    }

    if let Some(T![@]) = p.peek() {
        directive::directives(p, Constness::NotConst);
    }

    match p.peek() {
        Some(T!['{']) => selection::selection_set(p),
        _ => p.err("expected Selection Set"),
    }
}

/// See: https://spec.graphql.org/October2021/#FragmentSpread
///
/// *FragmentSpread*:
///     **...** FragmentName Directives?
pub(crate) fn fragment_spread(p: &mut Parser) {
    let _g = p.start_node(SyntaxKind::FRAGMENT_SPREAD);
    p.bump(S![...]);

    match p.peek() {
        Some(TokenKind::Name) => {
            fragment_name(p);
        }
        _ => p.err("expected a Name"),
    }

    if let Some(T![@]) = p.peek() {
        directive::directives(p, Constness::NotConst);
    }
}

#[cfg(test)]
mod test {
    use crate::cst;
    use crate::Parser;

    #[test]
    fn it_parses_fragment_description_when_enabled() {
        let input = "\"Fragment description\"\nfragment ProfilePic on User { id }";
        let cst = Parser::new(input)
            .allow_executable_descriptions(true)
            .parse();

        assert_eq!(cst.errors().len(), 0);
        let def = cst.document().definitions().next().unwrap();
        let cst::Definition::FragmentDefinition(frag) = def else {
            panic!("expected a FragmentDefinition");
        };
        assert!(frag.description().is_some());
    }

    #[test]
    fn it_parses_legacy_fragment_variables_when_enabled() {
        let input = "fragment ProfilePic($size: Int = 32) on User { profilePic(size: $size) }";
        let cst = Parser::new(input)
            .allow_legacy_fragment_variables(true)
            .parse();

        assert_eq!(cst.errors().len(), 0);
        let def = cst.document().definitions().next().unwrap();
        let cst::Definition::FragmentDefinition(frag) = def else {
            panic!("expected a FragmentDefinition");
        };
        assert!(frag.variable_definitions().is_some());
    }

    #[test]
    fn it_errors_on_legacy_fragment_variables_when_disabled() {
        // Default parser: variable definitions on a fragment are rejected.
        let input = "fragment ProfilePic($size: Int) on User { id }";
        let cst = Parser::new(input).parse();

        assert_ne!(cst.errors().len(), 0);
    }
}