slovo/compiler/src/scaffold.rs
2026-05-22 08:38:43 +02:00

148 lines
4.0 KiB
Rust

use std::{
fs, io,
path::{Path, PathBuf},
};
use crate::diag::Diagnostic;
pub fn create_project(target: &str, explicit_name: Option<&str>) -> Result<(), Diagnostic> {
if target.trim().is_empty() {
return Err(Diagnostic::new(
target,
"UsageError",
"project directory must not be empty",
));
}
let root = Path::new(target);
if root.is_file() {
return Err(Diagnostic::new(
target,
"ProjectScaffoldBlocked",
format!(
"cannot create project `{}` because a file exists there",
target
),
));
}
if root.is_dir() && has_entries(root)? {
return Err(Diagnostic::new(
target,
"ProjectScaffoldBlocked",
format!(
"cannot create project `{}` because the directory is not empty",
target
),
)
.hint("choose an empty directory or a new project path"));
}
let raw_name = explicit_name
.map(str::to_string)
.unwrap_or_else(|| basename(root).unwrap_or_else(|| "slovo-project".to_string()));
let name = sanitize_project_name(&raw_name);
if name.is_empty() {
return Err(Diagnostic::new(
target,
"InvalidProjectName",
"project name must contain at least one ASCII letter or digit",
));
}
let src = root.join("src");
create_dir_all_checked(&src, target)?;
write_checked(
&root.join("slovo.toml"),
&format!(
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
name
),
target,
)?;
write_checked(
&src.join("main.slo"),
"(module main)\n\n(fn main () -> i32\n 0)\n\n(test \"main returns zero\"\n (= (main) 0))\n",
target,
)
}
fn has_entries(path: &Path) -> Result<bool, Diagnostic> {
let mut entries = fs::read_dir(path).map_err(|err| io_diagnostic(path, err))?;
match entries.next() {
Some(Ok(_)) => Ok(true),
Some(Err(err)) => Err(io_diagnostic(path, err)),
None => Ok(false),
}
}
fn basename(path: &Path) -> Option<String> {
path.file_name()
.and_then(|name| name.to_str())
.map(str::to_string)
}
fn sanitize_project_name(value: &str) -> String {
let mut out = String::new();
let mut previous_dash = false;
for ch in value.chars().flat_map(char::to_lowercase) {
let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
Some(ch)
} else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() {
Some('-')
} else {
None
};
let Some(ch) = mapped else {
continue;
};
if ch == '-' {
if !out.is_empty() && !previous_dash {
out.push('-');
previous_dash = true;
}
} else {
out.push(ch);
previous_dash = false;
}
}
while out.ends_with('-') {
out.pop();
}
if out
.chars()
.next()
.is_some_and(|first| first.is_ascii_digit())
{
out.insert_str(0, "project-");
}
out
}
fn create_dir_all_checked(path: &PathBuf, target: &str) -> Result<(), Diagnostic> {
fs::create_dir_all(path).map_err(|err| {
Diagnostic::new(
target,
"ProjectScaffoldBlocked",
format!("cannot create directory `{}`: {}", path.display(), err),
)
})
}
fn write_checked(path: &Path, text: &str, target: &str) -> Result<(), Diagnostic> {
fs::write(path, text).map_err(|err| {
Diagnostic::new(
target,
"ProjectScaffoldBlocked",
format!("cannot write `{}`: {}", path.display(), err),
)
})
}
fn io_diagnostic(path: &Path, err: io::Error) -> Diagnostic {
Diagnostic::new(
path.display().to_string(),
"ProjectScaffoldBlocked",
format!("cannot inspect `{}`: {}", path.display(), err),
)
}