Skip to content

Parsing the empty string generates an error with out of bounds location #959

@samtipton

Description

@samtipton

Edit after investigation:

Example:

let input = ""; // The empty string
apollo_compiler::ast::Document::parse(input, "example.graphql").unwrap();
called `Result::unwrap()` on an `Err` value: Error: syntax error: Unexpected <EOF>.

The empty document being invalid is correct per spec, though it could be more user-friendly to return an empty document in this case. But if we do return an error, its string formatting should include a source location. For comparison:

let input = " "; // A single space
apollo_compiler::ast::Document::parse(input, "example.graphql").unwrap();
called `Result::unwrap()` on an `Err` value: Error: syntax error: Unexpected <EOF>.
   ╭─[ example.graphql:1:2 ]1 │ 
   │  │ 
   │  ╰─ Unexpected <EOF>.
───╯

The underlying bug is in apollo-parser:

let input = ""; // The empty string
dbg!(apollo_parser::Parser::new(input).parse().errors().next().unwrap().index());

The error index is 1 which it out of bounds. It should be 0 instead. When apollo-compiler formats a diagnostic, Ariadne ignores locations that are out of bound.

Original report


Description

In SchemaBuilder#built_in() we have the following

            let input = include_str!("../built_in_types.graphql").to_owned();
            let path = "built_in.graphql";
            let id = FileId::BUILT_IN;
            let ast = ast::Document::parser().parse_ast_inner(input, path, id, &mut builder.errors);
            // ..
            builder.add_ast_document(&ast, executable_definitions_are_errors);

However, path is incorrect.

This results in an error when we add additional documents to the schema_builder. Like for example,

        ast::Document::parser().parse_into_schema_builder(&schema_content, file_path.to_string_lossy().as_ref(), &mut schema_builder);

Steps to reproduce

use apollo_compiler::schema::SchemaBuilder;
use clap::Parser as ClapParser;
use std::fs;
use std::ops::Index;
use std::path::{Path, PathBuf};
use apollo_compiler::ast;

#[derive(ClapParser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Directory containing *.graphqls files
    #[arg(short = 'd', long)]
    schema_path: PathBuf,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();

    // get schema files from directory
    let schema_files = if cli.schema_path.is_dir() {
        collect_schema_files_from_directory(&cli.schema_path)?
    } else {
        return Err(format!("{} is not a directory", &cli.schema_path.display()).into());
    };

    let file_count = schema_files.len();
    if file_count == 0 {
        return Err("No schema files found".into());
    }

    // Create a schema builder to merge schemas
    let mut schema_builder = SchemaBuilder::new();

    // Read and merge all schema files
    for file_path in &schema_files {
        println!("Reading schema from: {}", file_path.display());
        let schema_content = fs::read_to_string(file_path)?;

        // Add to schema builder
        ast::Document::parser().parse_into_schema_builder(&schema_content, file_path.to_string_lossy().as_ref(), &mut schema_builder);
    }

    // Build the final merged schema
    let merged_schema = schema_builder.build();

    // Check for validation errors
    match merged_schema {
        Ok(_) => {
            println!("Successfully validated and merged {} schema files",
                     file_count);

            // Convert the merged schema to SDL (String)
            let merged_schema_sdl = merged_schema.unwrap().to_string();

            println!("Merged schema:");
            println!("{}", merged_schema_sdl);
        },
        Err(errors) => {
            println!("Schema validation errors:");
            for (index, error) in errors.errors.iter().enumerate() {
                println!("{}: {}", error.sources.index(index).path().display(), error);
            }
            return Err("Schema validation failed".into());
        }
    }

    Ok(())
}

/// Collect all *.graphqls files from a directory
fn collect_schema_files_from_directory(dir: &Path) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
    // Check if composite-schema.graphqls exists
    let composite_path = dir.join("composite-schema.graphqls");
    if composite_path.exists() {
        println!("Found composite schema file, using only this file");
        return Ok(vec![composite_path]);
    }

    // Otherwise collect all *.graphqls files
    let mut schema_files = Vec::new();

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_file() {
            if let Some(extension) = path.extension() {
                if extension == "graphqls" {
                    schema_files.push(path);
                }
            }
        }
    }

    if schema_files.is_empty() {
        println!("No *.graphqls files found in directory: {}", dir.display());
    } else {
        println!("Found {} schema files in directory", schema_files.len());
    }

    Ok(schema_files)
}

Expected result

A schema definition string in output

Actual result

Schema validation errors:
built_in.graphql: Error: syntax error: Unexpected <EOF>.

Environment

  • Operating system and version: Mac OSX Sequoia 15.3.1
  • Shell (bash/zsh/powershell): zsh
  • apollo-rs crate: apollo_compiler
  • Crate version: 1.27.0

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions