use std::io;
use std::path::Path;
use std::sync::Arc;
use crate::driver::CanonSourceFile;
use crate::error::{Error, RichError, WithSpan as _};
use crate::parse::UseDecl;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct SourceFile {
name: Option<Arc<Path>>,
content: Arc<str>,
}
impl From<(&Path, &str)> for SourceFile {
fn from((name, content): (&Path, &str)) -> Self {
Self::new(name, Arc::from(content))
}
}
impl From<CanonSourceFile> for SourceFile {
fn from(canon_source: CanonSourceFile) -> Self {
Self::new(canon_source.name().as_path(), canon_source.content())
}
}
impl SourceFile {
pub fn new(name: &Path, content: Arc<str>) -> Self {
Self {
name: Some(Arc::from(name)),
content,
}
}
pub fn anonymous(content: Arc<str>) -> Self {
Self {
name: None,
content,
}
}
pub fn name(&self) -> &Option<Arc<Path>> {
&self.name
}
pub fn content(&self) -> Arc<str> {
self.content.clone()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CanonPath(Arc<Path>);
impl CanonPath {
pub fn canonicalize(path: &Path) -> Result<Self, String> {
let canon_path = std::fs::canonicalize(path).map_err(|err| {
format!(
"Failed to find library target path '{}' :{}",
path.display(),
err
)
})?;
Ok(Self(Arc::from(canon_path.as_path())))
}
pub fn join(&self, parts: &[&str]) -> Result<Self, String> {
let mut new_path = self.0.to_path_buf();
for part in parts {
new_path.push(part);
}
Self::canonicalize(&new_path.with_extension("simf"))
}
pub fn starts_with(&self, path: &CanonPath) -> bool {
self.as_path().starts_with(path.as_path())
}
pub fn as_path(&self) -> &Path {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct Remapping {
pub context_prefix: CanonPath,
pub drp_name: String,
pub target: CanonPath,
}
#[derive(Debug, Clone, Default)]
pub struct DependencyMap {
inner: Vec<Remapping>,
}
impl DependencyMap {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
fn sort_mappings(&mut self) {
self.inner.sort_by(|a, b| {
let len_a = a.context_prefix.as_path().as_os_str().len();
let len_b = b.context_prefix.as_path().as_os_str().len();
len_b.cmp(&len_a)
});
}
pub fn insert(
&mut self,
context: CanonPath,
drp_name: String,
target: CanonPath,
) -> io::Result<()> {
self.inner.push(Remapping {
context_prefix: context,
drp_name,
target,
});
self.sort_mappings();
Ok(())
}
pub fn resolve_path(
&self,
current_file: &CanonPath,
use_decl: &UseDecl,
) -> Result<CanonPath, RichError> {
let parts = use_decl.path();
let drp_name = use_decl.drp_name()?;
for remapping in &self.inner {
if !current_file.starts_with(&remapping.context_prefix) {
continue;
}
if remapping.drp_name == drp_name {
return Self::build_and_verify_path(&remapping.target, &parts[1..]).map_err(
|failed_path| {
RichError::new(Error::FileNotFound(failed_path), *use_decl.span())
},
);
}
}
Err(Error::UnknownLibrary(drp_name.to_string())).with_span(*use_decl.span())
}
fn build_and_verify_path(
base_target: &CanonPath,
module_parts: &[impl ToString],
) -> Result<CanonPath, std::path::PathBuf> {
let mut theoretical_path = base_target.as_path().to_path_buf();
for part in module_parts {
theoretical_path.push(part.to_string());
}
theoretical_path.set_extension("simf");
match CanonPath::canonicalize(&theoretical_path) {
Ok(valid_canon_path) => Ok(valid_canon_path),
Err(_) => Err(theoretical_path),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use crate::str::Identifier;
use crate::test_utils::TempWorkspace;
use super::*;
pub fn canon(p: &Path) -> CanonPath {
CanonPath::canonicalize(p).unwrap()
}
impl CanonPath {
pub fn dummy_for_test(path: &Path) -> Self {
Self(Arc::from(path))
}
}
fn create_dummy_use_decl(path_segments: &[&str]) -> UseDecl {
let path: Vec<Identifier> = path_segments
.iter()
.map(|&s| Identifier::dummy(s))
.collect();
UseDecl::dummy_path(path)
}
#[test]
fn test_sorting_longest_prefix() {
let ws = TempWorkspace::new("sorting");
let workspace_dir = canon(&ws.create_dir("workspace"));
let nested_dir = canon(&ws.create_dir("workspace/project_a/nested"));
let project_a_dir = canon(&ws.create_dir("workspace/project_a"));
let target_v1 = canon(&ws.create_dir("lib/math_v1"));
let target_v3 = canon(&ws.create_dir("lib/math_v3"));
let target_v2 = canon(&ws.create_dir("lib/math_v2"));
let mut map = DependencyMap::new();
map.insert(workspace_dir.clone(), "math".to_string(), target_v1)
.unwrap();
map.insert(nested_dir.clone(), "math".to_string(), target_v3)
.unwrap();
map.insert(project_a_dir.clone(), "math".to_string(), target_v2)
.unwrap();
assert_eq!(map.inner[0].context_prefix, nested_dir);
assert_eq!(map.inner[1].context_prefix, project_a_dir);
assert_eq!(map.inner[2].context_prefix, workspace_dir);
}
#[test]
fn test_context_isolation() {
let ws = TempWorkspace::new("isolation");
let project_a = canon(&ws.create_dir("project_a"));
let target_utils = canon(&ws.create_dir("libs/utils_a"));
let current_file = canon(&ws.create_file("project_b/main.simf", ""));
let mut map = DependencyMap::new();
map.insert(project_a, "utils".to_string(), target_utils)
.unwrap();
let use_decl = create_dummy_use_decl(&["utils"]);
let result = map.resolve_path(¤t_file, &use_decl);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().error(),
Error::UnknownLibrary(..)
));
}
#[test]
fn test_resolve_longest_prefix_match() {
let ws = TempWorkspace::new("resolve_prefix");
let global_context = canon(&ws.create_dir("workspace"));
let global_target = canon(&ws.create_dir("libs/global_math"));
let global_expected = canon(&ws.create_file("libs/global_math/vector.simf", ""));
let frontend_context = canon(&ws.create_dir("workspace/frontend"));
let frontend_target = canon(&ws.create_dir("libs/frontend_math"));
let frontend_expected = canon(&ws.create_file("libs/frontend_math/vector.simf", ""));
let mut map = DependencyMap::new();
map.insert(global_context, "math".to_string(), global_target)
.unwrap();
map.insert(frontend_context, "math".to_string(), frontend_target)
.unwrap();
let use_decl = create_dummy_use_decl(&["math", "vector"]);
let frontend_file = canon(&ws.create_file("workspace/frontend/src/main.simf", ""));
let resolved_frontend = map.resolve_path(&frontend_file, &use_decl).unwrap();
assert_eq!(resolved_frontend, frontend_expected);
let backend_file = canon(&ws.create_file("workspace/backend/src/main.simf", ""));
let resolved_backend = map.resolve_path(&backend_file, &use_decl).unwrap();
assert_eq!(resolved_backend, global_expected);
}
#[test]
fn test_resolve_relative_current_file_against_canonical_context() {
let ws = TempWorkspace::new("relative_current");
let context = canon(&ws.create_dir("workspace/frontend"));
let target = canon(&ws.create_dir("libs/frontend_math"));
let expected = canon(&ws.create_file("libs/frontend_math/vector.simf", ""));
let current_file = canon(&ws.create_file("workspace/frontend/src/main.simf", ""));
let mut map = DependencyMap::new();
map.insert(context, "math".to_string(), target).unwrap();
let use_decl = create_dummy_use_decl(&["math", "vector"]);
let result = map.resolve_path(¤t_file, &use_decl).unwrap();
assert_eq!(result, expected);
}
}