use crate::{Htl, ModuleCandidate, ModuleKind, same_file};
use anyhow::Result;
use serde::Serialize;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Read,
Shadowed,
Runtime,
}
impl Status {
pub fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Shadowed => "shadowed",
Self::Runtime => "runtime",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OriginKind {
Crate,
Dependency,
Vendored,
Patched,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Origin {
pub kind: OriginKind,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
impl Origin {
pub fn describe(&self) -> String {
let what = match self.kind {
OriginKind::Crate => "shipped by",
OriginKind::Dependency => "installed from",
OriginKind::Vendored => "vendored copy of",
OriginKind::Patched => "patched",
};
match &self.version {
Some(v) => format!("{what} {} {v}", self.name),
None => format!("{what} {}", self.name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Candidate {
pub order: usize,
pub path: String,
pub dir: String,
pub kind: ModuleKind,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub shadowed_by: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<Origin>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Summary {
pub candidates: usize,
pub shadowed: usize,
pub ok: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Resolution {
pub module: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub read: Option<String>,
pub candidates: Vec<Candidate>,
pub searched: Vec<String>,
pub summary: Summary,
}
pub fn resolve(
h: &Htl,
name: &str,
root: Option<&Path>,
project: Option<&crate::pkg::Project>,
) -> Result<Resolution> {
let candidates = h.module_candidates(name)?;
let dirs = h.search_path_dirs()?;
let (read, lua) = h.resolve_module(name)?;
let read_kind = read.as_deref().map(ModuleKind::of_path);
let read_at = read
.as_ref()
.and_then(|r| candidates.iter().position(|c| same_file(&c.path, r)))
.map(|i| i + 1);
let rows = candidates
.iter()
.enumerate()
.map(|(i, c)| {
let order = i + 1;
let status = if read_at == Some(order) {
Status::Read
} else if read_kind == Some(ModuleKind::Declaration)
&& lua.as_ref().is_some_and(|l| same_file(&c.path, l))
{
Status::Runtime
} else {
Status::Shadowed
};
let dir = dir_of(c, &dirs);
Candidate {
order,
path: show(&c.path, root),
dir: show(&dir, root),
kind: c.kind,
status,
shadowed_by: (status == Status::Shadowed).then_some(read_at).flatten(),
origin: origin_of(&c.path, &dir, project),
}
})
.collect::<Vec<_>>();
let shadowed = rows.iter().filter(|c| c.status == Status::Shadowed).count();
let mut searched: Vec<String> = Vec::new();
for d in &dirs {
let d = show(d, root);
if !searched.contains(&d) {
searched.push(d);
}
}
Ok(Resolution {
module: name.to_string(),
read: read.as_ref().map(|p| show(p, root)),
searched,
summary: Summary {
candidates: rows.len(),
shadowed,
ok: read.is_some(),
},
candidates: rows,
})
}
impl ModuleKind {
fn of_path(p: &Path) -> Self {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name.ends_with(".d.tl") {
Self::Declaration
} else if name.ends_with(".tl") {
Self::Source
} else {
Self::Lua
}
}
}
fn dir_of(c: &ModuleCandidate, dirs: &[PathBuf]) -> PathBuf {
let p = canon(&c.path);
dirs.iter()
.filter(|d| p.starts_with(canon(d)))
.max_by_key(|d| canon(d).components().count())
.cloned()
.unwrap_or_else(|| c.dir.clone())
}
fn origin_of(path: &Path, dir: &Path, project: Option<&crate::pkg::Project>) -> Option<Origin> {
if let Some(note) = crate::dep_dts::Note::read(dir) {
return Some(Origin {
kind: OriginKind::Crate,
name: note.package,
version: Some(note.version),
});
}
let p = project?;
if let Some(name) = under(path, &p.entries).or_else(|| under(path, &p.vendored)) {
return Some(Origin {
kind: OriginKind::Dependency,
name,
version: None,
});
}
for copy in &p.vendored_copies {
if starts_with(path, copy) {
return Some(Origin {
kind: OriginKind::Vendored,
name: dir_name(copy),
version: None,
});
}
}
for patch in &p.patches {
if starts_with(path, &patch.dir) {
return Some(Origin {
kind: OriginKind::Patched,
name: patch.name.clone(),
version: None,
});
}
}
None
}
fn under(path: &Path, dir: &Path) -> Option<String> {
let (p, d) = (canon(path), canon(dir));
let rest = p.strip_prefix(&d).ok()?;
Some(
rest.components()
.next()?
.as_os_str()
.to_string_lossy()
.into(),
)
}
fn starts_with(path: &Path, dir: &Path) -> bool {
canon(path).starts_with(canon(dir))
}
fn dir_name(p: &Path) -> String {
p.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| p.display().to_string())
}
fn canon(p: &Path) -> PathBuf {
std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
}
fn show(p: &Path, root: Option<&Path>) -> String {
let Some(root) = root else {
return p.display().to_string();
};
let (a, b) = (canon(p), canon(root));
match a.strip_prefix(&b) {
Ok(rest) if rest.as_os_str().is_empty() => ".".to_string(),
Ok(rest) => rest.display().to_string(),
Err(_) => p.display().to_string(),
}
}