knave-expand 0.14.22

The reading, scoping and expansion stages of knave
Documentation
mod block;
mod document;
mod import_order;
mod layout;
mod list_plan;
mod meaning;
mod measure;
mod shape_reader;
mod shapes;
mod syntax_tree;

pub use shape_reader::ModuleLookup;
pub use shapes::{Shape, ShapeCatalog, ShapeStatement};

use crate::diagnostic::Diagnostics;
use crate::reader::{Form, Reader};
use crate::source::{SourceFile, SourceMap};
use document::DocumentPrinter;
use import_order::ImportSorter;
use layout::LayoutEngine;
use meaning::MeaningComparison;
use shape_reader::ShapeReader;
use syntax_tree::SyntaxTree;

#[derive(Default)]
pub struct SourceFormatter {
    shape_reader: ShapeReader,
}

#[derive(Debug)]
pub enum FormatFailure {
    Unreadable(Diagnostics),
    ChangedMeaning,
    Unstable,
}

struct FormattedText<'s> {
    shapes: &'s ShapeCatalog,
    text: String,
}

impl SourceFormatter {
    const FORMATTED_SOURCE: &'static str = "<formatted>";

    pub fn format(
        &mut self,
        source: &SourceFile,
        lookup: &ModuleLookup,
    ) -> Result<String, FormatFailure> {
        let written = Reader::new(source)
            .read()
            .map_err(FormatFailure::Unreadable)?;
        let shapes = self.shape_reader.catalog(&written, lookup);
        let formatted = FormattedText::lay_out(&shapes, source.text());
        formatted.verify(&written, source.text())?;
        Ok(formatted.text)
    }
}

impl<'s> FormattedText<'s> {
    fn lay_out(shapes: &'s ShapeCatalog, text: &str) -> Self {
        let mut tree = SyntaxTree::build(text);
        ImportSorter::sort(&mut tree);
        let mut engine = LayoutEngine::new(shapes);
        let text = DocumentPrinter::new(&mut engine).print(&tree);
        Self { shapes, text }
    }

    fn verify(&self, written: &[Form], written_text: &str) -> Result<(), FormatFailure> {
        let comparison = MeaningComparison::new(self.shapes);
        let keeps_meaning = comparison.same_comments(written_text, &self.text)
            && self
                .read()
                .is_some_and(|formatted| comparison.same_forms(written, &formatted));
        if !keeps_meaning {
            return Err(FormatFailure::ChangedMeaning);
        }
        if Self::lay_out(self.shapes, &self.text).text != self.text {
            return Err(FormatFailure::Unstable);
        }
        Ok(())
    }

    fn read(&self) -> Option<Vec<Form>> {
        let mut sources = SourceMap::default();
        let source = sources.add(SourceFormatter::FORMATTED_SOURCE, self.text.as_str());
        Reader::new(sources.file(source)).read().ok()
    }
}

#[cfg(test)]
mod tests {
    use super::{FormatFailure, FormattedText, ShapeCatalog, ShapeStatement};
    use crate::reader::{Form, Reader};
    use crate::source::SourceMap;

    struct CheckFixture {
        shapes: ShapeCatalog,
    }

    impl CheckFixture {
        fn with_docstring_shape() -> Self {
            let mut shapes = ShapeCatalog::default();
            let statement = ShapeStatement::of(
                &Self::forms("(header-shape (defn name parameters) :docstring)")[0],
            )
            .expect("the statement states a shape");
            shapes.add(&statement);
            Self { shapes }
        }

        fn verify(&self, written: &str, formatted: &str) -> Result<(), FormatFailure> {
            FormattedText {
                shapes: &self.shapes,
                text: formatted.to_string(),
            }
            .verify(&Self::forms(written), written)
        }

        fn forms(text: &str) -> Vec<Form> {
            let mut sources = SourceMap::default();
            let source = sources.add("written", text);
            Reader::new(sources.file(source))
                .read()
                .unwrap_or_else(|_| panic!("the text reads"))
        }
    }

    #[test]
    fn text_that_reads_to_other_forms_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(add 1 2)", "(add 1 3)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn text_that_lays_out_differently_again_is_unstable() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(add 1 2)", "(add 1\n     2)\n"),
            Err(FormatFailure::Unstable)
        ));
    }

    #[test]
    fn a_moved_docstring_line_keeps_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        let written = "(defn f ()\n      \"Doc.\n      More.\"\n      1)";
        let formatted = "(defn f ()\n  \"Doc.\n  More.\"\n  1)\n";
        assert!(fixture.verify(written, formatted).is_ok());
    }

    #[test]
    fn a_moved_line_of_a_string_that_is_no_docstring_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        let written = "(def f\n      \"Text.\n      More.\")";
        let formatted = "(def f\n  \"Text.\n  More.\")\n";
        assert!(matches!(
            fixture.verify(written, formatted),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn a_number_keeps_its_written_text() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(add 1_000)", "(add 1000)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn text_that_drops_a_comment_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(f :key ; why\n   value)", "(f :key value)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn a_moved_line_of_a_last_string_in_docstring_position_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        let written = "(defn f ()\n      \"Text.\n      More.\")";
        let formatted = "(defn f ()\n  \"Text.\n  More.\")\n";
        assert!(matches!(
            fixture.verify(written, formatted),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn text_that_merges_two_comments_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(f (g a ; c1\n) ; c2\n b)", "(f (g a) ; c1 ; c2\n   b)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn sorted_import_names_keep_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(
            fixture
                .verify("(use m b a)\n(export d c)", "(use m a b)\n\n(export c d)\n")
                .is_ok()
        );
    }

    #[test]
    fn an_import_list_with_another_name_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(use m b a)", "(use m a c)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn an_import_from_another_module_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(use m b a)", "(use n a b)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn a_reordered_call_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(f b a)", "(f a b)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn a_list_that_loses_a_form_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(f a b)", "(f a)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn a_reordered_quoted_import_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify(
                "(def x (quote (use m b a)))",
                "(def x (quote (use m a b)))\n"
            ),
            Err(FormatFailure::ChangedMeaning)
        ));
    }

    #[test]
    fn an_import_that_loses_a_name_changes_the_meaning() {
        let fixture = CheckFixture::with_docstring_shape();
        assert!(matches!(
            fixture.verify("(use m a b)", "(use m a)\n"),
            Err(FormatFailure::ChangedMeaning)
        ));
    }
}