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