knave 0.12.6

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::fmt;
use std::io::{self, Read, Write};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::mpsc::{self, Receiver};
use std::thread::{self, JoinHandle};
use std::time::Duration;

use super::interrupt_signal::InterruptSignal;
use super::token_alignment::TokenAlignment;
use crate::emit::{EmittedCrate, FormattingWork};
use crate::language::Language;

pub struct EmittedFormatting {
    rustfmt: Rustfmt,
}

impl EmittedFormatting {
    pub const TIME_LIMIT: Duration = Duration::from_secs(10);

    pub fn within(time_limit: Duration) -> Self {
        Self {
            rustfmt: Rustfmt::new(time_limit),
        }
    }

    #[inline(never)]
    pub fn apply(&self, emitted: &EmittedCrate) -> Result<EmittedCrate, FormattingError> {
        let code = self.rustfmt.format(&emitted.code)?;
        let alignment = TokenAlignment::new(&emitted.code, &code);
        let span_map = alignment
            .remap(&emitted.span_map)
            .ok_or(FormattingError::Misaligned)?;
        Ok(EmittedCrate {
            code,
            span_map,
            names: emitted.names.clone(),
            emission: emitted.emission,
            formatting: Some(FormattingWork {
                scanned_tokens: alignment.scanned_tokens(),
            }),
        })
    }

    pub fn time_limit_variable() -> String {
        format!(
            "{}_RUSTFMT_TIME_LIMIT_MILLISECONDS",
            Language::NAME.to_uppercase()
        )
    }
}

#[derive(Debug)]
pub enum FormattingError {
    Unavailable(io::Error),
    Failed(String),
    TimedOut(Duration),
    Misaligned,
    Interrupted,
}

impl fmt::Display for FormattingError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Unavailable(error) => write!(formatter, "cannot run rustfmt: {error}"),
            Self::Failed(message) if message.is_empty() => write!(formatter, "rustfmt failed"),
            Self::Failed(message) => write!(formatter, "rustfmt failed: {message}"),
            Self::TimedOut(limit) => write!(formatter, "rustfmt did not finish within {limit:?}"),
            Self::Interrupted => write!(formatter, "rustfmt was interrupted"),
            Self::Misaligned => {
                write!(
                    formatter,
                    "cannot align rustfmt's output with the emitted Rust"
                )
            }
        }
    }
}

struct Rustfmt {
    time_limit: Duration,
}

impl Rustfmt {
    const PROGRAM: &'static str = "rustfmt";
    const ARGUMENTS: [&'static str; 2] = ["--edition", "2024"];

    fn new(time_limit: Duration) -> Self {
        Self { time_limit }
    }

    fn format(&self, source: &str) -> Result<String, FormattingError> {
        let mut child = Self::spawn()?;
        let output = PipeReader::start(child.stdout.take().expect("standard output is piped"));
        let errors = PipeReader::start(child.stderr.take().expect("standard error is piped"));
        let formatted = self.communicate(&mut child, source, &output, &errors);
        output.join();
        errors.join();
        formatted
    }

    fn communicate(
        &self,
        child: &mut Child,
        source: &str,
        output: &PipeReader,
        errors: &PipeReader,
    ) -> Result<String, FormattingError> {
        Self::send(child, source).map_err(|error| Self::explain_failed_send(child, error))?;
        let formatted = self
            .read_within_limit(output)
            .inspect_err(|_| _ = Self::stop(child))?;
        let status = child.wait().map_err(FormattingError::Unavailable)?;
        if InterruptSignal::stopped(status) {
            return Err(FormattingError::Interrupted);
        }
        if !status.success() {
            return Err(FormattingError::Failed(errors.read_message()));
        }
        String::from_utf8(formatted)
            .map_err(|_| FormattingError::Failed("rustfmt wrote non-UTF-8 output".to_string()))
    }

    fn spawn() -> Result<Child, FormattingError> {
        Command::new(Self::PROGRAM)
            .args(Self::ARGUMENTS)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .map_err(FormattingError::Unavailable)
    }

    fn send(child: &mut Child, source: &str) -> Result<(), FormattingError> {
        let mut input = child.stdin.take().expect("standard input is piped");
        input
            .write_all(source.as_bytes())
            .map_err(|error| FormattingError::Failed(error.to_string()))
    }

    fn explain_failed_send(child: &mut Child, error: FormattingError) -> FormattingError {
        let interrupted = Self::stop(child).is_some_and(InterruptSignal::stopped);
        if interrupted {
            FormattingError::Interrupted
        } else {
            error
        }
    }

    fn read_within_limit(&self, output: &PipeReader) -> Result<Vec<u8>, FormattingError> {
        output
            .read_within(self.time_limit)
            .ok_or(FormattingError::TimedOut(self.time_limit))?
            .map_err(|error| FormattingError::Failed(error.to_string()))
    }

    fn stop(child: &mut Child) -> Option<ExitStatus> {
        child.kill().ok();
        child.wait().ok()
    }
}

struct PipeReader {
    contents: Receiver<io::Result<Vec<u8>>>,
    thread: Option<JoinHandle<()>>,
}

impl PipeReader {
    fn start(mut pipe: impl Read + Send + 'static) -> Self {
        let (sender, contents) = mpsc::channel();
        let thread = thread::spawn(move || {
            let mut bytes = Vec::new();
            let read = pipe.read_to_end(&mut bytes).map(|_| bytes);
            sender.send(read).ok();
        });
        Self {
            contents,
            thread: Some(thread),
        }
    }

    fn read_within(&self, time_limit: Duration) -> Option<io::Result<Vec<u8>>> {
        self.contents.recv_timeout(time_limit).ok()
    }

    fn read_message(&self) -> String {
        let bytes = self.contents.recv().ok().and_then(Result::ok);
        String::from_utf8_lossy(&bytes.unwrap_or_default())
            .trim()
            .to_string()
    }

    fn join(mut self) {
        let thread = self.thread.take().expect("only join takes the thread");
        thread.join().ok();
    }
}

impl Drop for PipeReader {
    fn drop(&mut self) {
        debug_assert!(
            self.thread.is_none(),
            "each pipe reader is joined before it drops"
        );
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    use super::*;

    struct EmptyPipe {
        dropped: Arc<AtomicBool>,
    }

    impl EmptyPipe {
        fn new(dropped: &Arc<AtomicBool>) -> Self {
            Self {
                dropped: Arc::clone(dropped),
            }
        }
    }

    impl Read for EmptyPipe {
        fn read(&mut self, _buffer: &mut [u8]) -> io::Result<usize> {
            Ok(0)
        }
    }

    impl Drop for EmptyPipe {
        fn drop(&mut self) {
            self.dropped.store(true, Ordering::SeqCst);
        }
    }

    #[test]
    fn an_interrupted_rustfmt_names_the_interruption() {
        assert_eq!(
            FormattingError::Interrupted.to_string(),
            "rustfmt was interrupted"
        );
    }

    #[test]
    fn join_waits_until_the_reader_drops_its_pipe() {
        let dropped = Arc::new(AtomicBool::new(false));
        PipeReader::start(EmptyPipe::new(&dropped)).join();
        assert!(dropped.load(Ordering::SeqCst));
    }

    #[test]
    fn describes_each_formatting_failure() {
        let unavailable = FormattingError::Unavailable(io::Error::from(io::ErrorKind::NotFound));
        assert!(unavailable.to_string().contains("rustfmt"));
        let failed = FormattingError::Failed("boom".to_string());
        assert!(failed.to_string().contains("boom"));
        let empty = FormattingError::Failed(String::new());
        assert_eq!(empty.to_string(), "rustfmt failed");
        let misaligned = FormattingError::Misaligned;
        assert!(misaligned.to_string().contains("align"));
    }

    #[test]
    fn names_the_time_limit_that_rustfmt_exceeded() {
        let timed_out = FormattingError::TimedOut(EmittedFormatting::TIME_LIMIT);
        assert_eq!(timed_out.to_string(), "rustfmt did not finish within 10s");
    }
}