slicec 0.4.0

The Slice parser and other core components for Slice compilers.
Documentation
// Copyright (c) ZeroC, Inc.

//! This module contains helper functions for the slicec test-suite.

// It's fine if a test doesn't need all of these functions.
#![allow(dead_code)]

use slicec::ast::Ast;
use slicec::compilation_state::CompilationState;
use slicec::compile_from_strings;
use slicec::diagnostics::Diagnostic;
use slicec::slice_options::SliceOptions;

/// This function parses the provided Slice file.
/// It is the lowest level test helper function, returning a full [`CompilationState`] instead of only part of it.
/// It also allows tests to configure the compiler by passing in [`SliceOptions`].
#[must_use]
pub fn parse(slice: impl Into<String>, options: Option<&SliceOptions>) -> CompilationState {
    compile_from_strings(&[&slice.into()], options)
}

/// This function parses the provided Slice file and returns the AST generated by doing so.
/// If any errors are encountered during parsing, it panics.
#[must_use]
pub fn parse_for_ast(slice: impl Into<String>) -> Ast {
    let compilation_state = parse(slice, None);
    if compilation_state.diagnostics.has_errors() {
        panic!("{:?}", compilation_state.diagnostics);
    }
    compilation_state.ast
}

/// This function parses the provided Slice files and returns the AST generated by doing so.
/// Each string is treated as a separate Slice file by the parser.
#[must_use]
pub fn parse_multiple_for_ast(slice: &[&str]) -> Ast {
    let compilation_state = compile_from_strings(slice, None);
    if !compilation_state.diagnostics.is_empty() {
        panic!("{:?}", compilation_state.diagnostics);
    }
    compilation_state.ast
}

/// This function parses the provided Slice file and returns any Diagnostics that were emitted during parsing.
#[must_use]
pub fn parse_for_diagnostics(slice: impl Into<String>) -> Vec<Diagnostic> {
    parse_multiple_for_diagnostics(&[&slice.into()])
}

/// This function parses the provided Slice files and returns any Diagnostics that were emitted during parsing.
/// Each string is treated as a separate Slice file by the parser.
#[must_use]
pub fn parse_multiple_for_diagnostics(slice: &[&str]) -> Vec<Diagnostic> {
    compile_from_strings(slice, None).diagnostics.into_inner()
}

/// Asserts that the provided slice parses okay, producing no diagnostics.
pub fn assert_parses(slice: impl Into<String>) {
    let diagnostics = parse_for_diagnostics(slice);
    let expected: [Diagnostic; 0] = []; // Compiler needs the type hint.
    check_diagnostics(diagnostics, expected);
}

/// Compares diagnostics emitted by the compiler to an array of expected diagnostics.
/// It ensures that the expected number of diagnostics were emitted (ie: that both lists are the same length).
///
/// If the correct number were emitted, it checks each diagnostic against the expected array in order.
/// For each diagnostic we ensure:
/// - It has the correct diagnostic code.
/// - It has the correct message.
/// - If a span was expected, that it has the correct span.
/// - If notes are expected, we check that all the notes have correct messages and spans.
///
/// If the expected diagnostics don't include spans or notes, this function doesn't check them.
/// This is useful for the majority of tests that aren't explicitly testing spans or notes.
pub fn check_diagnostics<const L: usize>(diagnostics: Vec<Diagnostic>, expected: [impl Into<Diagnostic>; L]) {
    // Check that the correct number of diagnostics were emitted.
    if expected.len() != diagnostics.len() {
        eprintln!(
            "Expected {} diagnostics, but got {}.",
            expected.len(),
            diagnostics.len()
        );
        eprintln!("The emitted diagnostics were:");
        for diagnostic in diagnostics {
            eprintln!("\t{diagnostic:?}");
        }
        eprintln!();
        panic!("test failure");
    }

    // Check that the emitted diagnostics match what was expected.
    for (expect, diagnostic) in expected.into_iter().zip(diagnostics) {
        let expect: Diagnostic = expect.into();
        let mut failed = false;

        // Check that the diagnostic codes match.
        if expect.code() != diagnostic.code() {
            eprintln!("diagnostic codes didn't match:");
            eprintln!("\texpected '{:?}', but got '{:?}'", expect.code(), diagnostic.code());
            failed = true;
        }

        // Check that the messages match.
        if expect.message() != diagnostic.message() {
            eprintln!("diagnostic messages didn't match:");
            eprintln!("\texpected: \"{}\"", expect.message());
            eprintln!("\t but got: \"{}\"", diagnostic.message());
            failed = true;
        }

        // If a span was provided, check that it matches.
        if expect.span.is_some() && expect.span != diagnostic.span {
            eprintln!("diagnostic spans didn't match:");
            eprintln!("\texpected: \"{:?}\"", expect.span);
            eprintln!("\t but got: \"{:?}\"", diagnostic.span);
            failed = true;
        }

        // If a scope was provided, check that it matches.
        if expect.scope.is_some() && expect.scope != diagnostic.scope {
            eprintln!("diagnostic scopes didn't match:");
            eprintln!("\texpected: \"{:?}\"", expect.scope);
            eprintln!("\t but got: \"{:?}\"", diagnostic.scope);
            failed = true;
        }

        // If notes were provided, check that they match.
        if !expect.notes.is_empty() {
            let expected_notes = expect.notes;
            let emitted_notes = diagnostic.notes;
            if expected_notes.len() != emitted_notes.len() {
                eprintln!(
                    "Expected {} notes, but got {}.",
                    expected_notes.len(),
                    emitted_notes.len()
                );
                eprintln!("The emitted notes were:");
                for note in emitted_notes {
                    eprintln!("\t{note:?}");
                }
                failed = true;
            } else {
                for (expected_note, emitted_note) in expected_notes.iter().zip(emitted_notes) {
                    // Check that the messages match.
                    if expected_note.message != emitted_note.message {
                        eprintln!("note messages didn't match:");
                        eprintln!("\texpected: \"{}\"", expected_note.message);
                        eprintln!("\t but got: \"{}\"", emitted_note.message);
                        failed = true;
                    }

                    // If a span was provided, check that it matches.
                    if expected_note.span.is_some() && expected_note.span != emitted_note.span {
                        eprintln!("note spans didn't match:");
                        eprintln!("\texpected: \"{:?}\"", expected_note.span);
                        eprintln!("\t but got: \"{:?}\"", emitted_note.span);
                        failed = true;
                    }
                }
            }
        }

        // If the checks failed, panic to signal a test failure.
        if failed {
            eprintln!();
            panic!("test failure");
        }
    }
}