knave 0.12.5

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use super::exit_status::ExitStatus;
use super::located_project::LocatedProject;
use super::program_file::ProgramFile;
use super::skipped_directory::SkippedDirectory;
use super::standard_error::StandardError;
use super::terminal::{ModuleOfSources, Terminal};
use crate::language::Language;

pub struct EntryTree {
    path: PathBuf,
    project: Option<LocatedProject>,
    skipped: SkippedDirectory,
    kind: TreeKind,
}

#[derive(Clone, Copy)]
enum TreeKind {
    Tests,
    Tasks,
}

impl TreeKind {
    const TESTS_ENTRY: &'static str = "<tests>";
    const TASKS_ENTRY: &'static str = "<tasks>";
    const UNNAMABLE: [char; 4] = ['(', ')', '"', ';'];

    fn stays_inside(self) -> bool {
        matches!(self, Self::Tasks)
    }

    fn refuses(self, module: &str) -> bool {
        self.stays_inside()
            && module
                .chars()
                .any(|character| character.is_whitespace() || Self::UNNAMABLE.contains(&character))
    }

    fn entry_name(self) -> &'static str {
        match self {
            Self::Tests => Self::TESTS_ENTRY,
            Self::Tasks => Self::TASKS_ENTRY,
        }
    }

    fn shares_source_paths(self) -> bool {
        matches!(self, Self::Tests)
    }

    fn program_file(self, project: &LocatedProject, file: ProgramFile) -> ProgramFile {
        match self {
            Self::Tests => project.test_file(file),
            Self::Tasks => project.task_file(file),
        }
    }
}

impl EntryTree {
    pub const TASKS_ENTRY: &'static str = TreeKind::TASKS_ENTRY;

    pub fn of_tests(path: PathBuf, project: Option<LocatedProject>) -> Self {
        let skipped = SkippedDirectory::for_walk_of(&path);
        Self {
            path,
            project,
            skipped,
            kind: TreeKind::Tests,
        }
    }

    pub fn of_tasks(project: LocatedProject) -> Self {
        Self {
            kind: TreeKind::Tasks,
            ..Self::of_tests(project.tasks_directory(), Some(project))
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn project(&self) -> Option<&LocatedProject> {
        self.project.as_ref()
    }

    pub fn load(&self, terminal: &mut Terminal) -> Result<ModuleOfSources, ExitStatus> {
        if self.path.is_dir() {
            self.load_directory(terminal)
        } else {
            Ok(ModuleOfSources {
                file: self.entry_file(),
                sources: vec![terminal.load(&self.path)?],
            })
        }
    }

    fn entry_file(&self) -> ProgramFile {
        match &self.project {
            Some(project) => project.test_file(project.file_within(self.path.clone())),
            None => ProgramFile::beside_itself(self.path.clone()),
        }
    }

    pub fn module_paths(&self) -> Result<Vec<String>, ExitStatus> {
        let mut modules = Vec::new();
        self.collect_modules(&self.path, &mut modules)
            .map_err(|error| Terminal::report_unreadable(&self.path, &error))?;
        modules.sort();
        match modules.iter().find(|module| self.kind.refuses(module)) {
            Some(module) => Err(self.report_unnamable(module)),
            None => Ok(modules),
        }
    }

    fn report_unnamable(&self, module: &str) -> ExitStatus {
        let file = self.path.join(format!("{module}.{}", Language::EXTENSION));
        StandardError::write_line(format_args!(
            "error: `{}` cannot be a module, because a module path cannot hold a space, a parenthesis, a quote or a semicolon\n\
             help: rename the file",
            file.display()
        ));
        ExitStatus::UsageError
    }

    fn load_directory(&self, terminal: &mut Terminal) -> Result<ModuleOfSources, ExitStatus> {
        let modules = self.module_paths()?;
        let uses: Vec<String> = modules
            .iter()
            .map(|module| format!("(use {module})"))
            .collect();
        let source = terminal.add_named_source(self.kind.entry_name(), &uses.join("\n"));
        Ok(ModuleOfSources {
            file: self.directory_file(),
            sources: vec![source],
        })
    }

    fn directory_file(&self) -> ProgramFile {
        let file = ProgramFile::within(self.path.join(self.kind.entry_name()), self.path.clone());
        match &self.project {
            Some(project) => self.kind.program_file(project, file),
            None => file,
        }
    }

    fn collect_modules(&self, directory: &Path, modules: &mut Vec<String>) -> io::Result<()> {
        for entry in fs::read_dir(directory)? {
            let path = entry?.path();
            if path.is_dir() {
                self.collect_subdirectory(&path, modules)?;
            } else if let Some(module) = self.module_path(&path) {
                modules.push(module);
            }
        }
        Ok(())
    }

    fn collect_subdirectory(&self, directory: &Path, modules: &mut Vec<String>) -> io::Result<()> {
        if self.skipped.is_skipped(directory) || self.leaves_the_tree(directory) {
            return Ok(());
        }
        self.collect_modules(directory, modules)
    }

    fn leaves_the_tree(&self, directory: &Path) -> bool {
        let inside = ProgramFile::canonical(directory)
            .zip(ProgramFile::canonical(&self.path))
            .is_some_and(|(directory, tree)| directory.starts_with(tree));
        self.kind.stays_inside() && !inside
    }

    fn module_path(&self, file: &Path) -> Option<String> {
        if file.extension()? != Language::EXTENSION || ProgramFile::is_project_file(file) {
            return None;
        }
        self.source_module_path(file)
            .or_else(|| ProgramFile::module_name(file.strip_prefix(&self.path).ok()?))
    }

    fn source_module_path(&self, file: &Path) -> Option<String> {
        let project = self
            .project
            .as_ref()
            .filter(|_| self.kind.shares_source_paths())?;
        ProgramFile::relative_module(file, &project.source_root())
    }
}