knave 0.12.10

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::core::ModulePath;
use crate::diagnostic::Diagnostics;
use crate::expand::ExpandProblems;
use crate::language::Language;
use crate::modules::ModuleSources;
use crate::source::Span;

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

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

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct TreeFile {
    module: String,
    path: PathBuf,
}

impl TreeFile {
    fn module_path(&self) -> ModulePath {
        ModulePath::new(self.module.as_str())
    }

    fn takes_compiler_module(&self) -> bool {
        !ModuleSources::is_written(&self.module_path())
    }
}

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 into_project(self) -> Option<LocatedProject> {
        self.project
    }

    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 files = self.module_files()?;
        Ok(files.into_iter().map(|file| file.module).collect())
    }

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

    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 files = self.module_files()?;
        Self::refuse_compiler_modules(&files, terminal)?;
        let uses: Vec<String> = files
            .iter()
            .map(|file| format!("(use {})", file.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 refuse_compiler_modules(
        files: &[TreeFile],
        terminal: &mut Terminal,
    ) -> Result<(), ExitStatus> {
        let mut problems = Diagnostics::default();
        for file in files.iter().filter(|file| file.takes_compiler_module()) {
            let start = Span::new(terminal.load(&file.path)?, 0..0);
            problems.push(ExpandProblems::file_takes_compiler_module(
                &file.module_path(),
                start,
            ));
        }
        if problems.has_errors() {
            return Err(terminal.report(&problems));
        }
        Ok(())
    }

    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, files: &mut Vec<TreeFile>) -> io::Result<()> {
        for entry in fs::read_dir(directory)? {
            let path = entry?.path();
            if path.is_dir() {
                self.collect_subdirectory(&path, files)?;
            } else if let Some(module) = self.module_path(&path) {
                files.push(TreeFile { module, path });
            }
        }
        Ok(())
    }

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

    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())
    }
}