use std::sync::Arc;
use crate::driver::CRATE_STR;
use crate::error::{Error, RichError, WithSpan as _};
use crate::parse::UseDecl;
use crate::source::CanonPath;
use crate::str::Identifier;
#[derive(Debug, Clone)]
pub(crate) struct Remapping {
pub(crate) context_prefix: CanonPath,
pub(crate) drp_name: String,
pub(crate) target: CanonPath,
}
fn is_valid_dependency_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return false;
}
!crate::lexer::is_keyword(s)
}
#[derive(Debug, Clone)]
pub struct DependencyMap {
remappings: Arc<Vec<Remapping>>,
package_roots: Vec<CanonPath>,
}
#[derive(Debug, Clone)]
pub struct ValidatedDeps {
remappings: Arc<Vec<Remapping>>,
dep_roots: Arc<[CanonPath]>,
}
impl ValidatedDeps {
pub fn with_root(&self, entry_root: CanonPath) -> Result<DependencyMap, Error> {
DependencyMapBuilder::validate_dir(&entry_root)?;
let mut package_roots: Vec<CanonPath> = std::iter::once(entry_root)
.chain(self.dep_roots.iter().cloned())
.collect();
package_roots.sort_by(Self::cmp_by_path_len_desc);
package_roots.dedup();
Ok(DependencyMap {
remappings: self.remappings.clone(),
package_roots,
})
}
fn cmp_by_path_len_desc(a: &CanonPath, b: &CanonPath) -> std::cmp::Ordering {
b.as_path()
.as_os_str()
.len()
.cmp(&a.as_path().as_os_str().len())
.then_with(|| a.cmp(b))
}
}
#[derive(Debug, Clone, Default)]
pub struct DependencyMapBuilder {
deps: Vec<Remapping>,
}
impl DependencyMapBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn build(self, entry_root: CanonPath) -> Result<DependencyMap, Error> {
self.validate_deps()?.with_root(entry_root)
}
pub fn validate_deps(self) -> Result<ValidatedDeps, Error> {
let mut seen =
std::collections::HashSet::<(CanonPath, String)>::with_capacity(self.deps.len());
let mut dep_roots = Vec::<CanonPath>::with_capacity(self.deps.len());
let mut remappings = Vec::<Remapping>::with_capacity(self.deps.len());
for dep in self.deps {
Self::validate_dir(&dep.context_prefix)?;
Self::validate_dir(&dep.target)?;
Self::validate_dependency_identifier(&dep.drp_name)?;
if !seen.insert((dep.context_prefix.clone(), dep.drp_name.clone())) {
return Err(Error::DuplicateDependencyAlias {
alias: dep.drp_name,
context: dep.context_prefix.as_path().display().to_string(),
});
}
dep_roots.push(dep.target.clone());
remappings.push(dep);
}
remappings.sort_by(|a, b| {
ValidatedDeps::cmp_by_path_len_desc(&a.context_prefix, &b.context_prefix)
.then_with(|| a.drp_name.cmp(&b.drp_name))
});
dep_roots.sort_by(ValidatedDeps::cmp_by_path_len_desc);
dep_roots.dedup();
Ok(ValidatedDeps {
remappings: Arc::new(remappings),
dep_roots: dep_roots.into(),
})
}
pub fn add_dependency(
&mut self,
context: CanonPath,
alias: String,
target: CanonPath,
) -> &mut Self {
self.deps.push(Remapping {
context_prefix: context,
drp_name: alias,
target,
});
self
}
fn validate_dir(path: &CanonPath) -> Result<(), Error> {
if !path.as_path().exists() {
return Err(Error::DependencyPathNotFound {
path: path.as_path().into(),
});
}
if !path.as_path().is_dir() {
return Err(Error::DependencyNotADirectory {
path: path.as_path().into(),
});
}
Ok(())
}
fn validate_dependency_identifier(alias: &str) -> Result<(), Error> {
if is_valid_dependency_identifier(alias) {
return Ok(());
}
if alias == CRATE_STR {
Err(Error::ReservedDependencyKeyword {
keyword: alias.to_string(),
})
} else {
Err(Error::InvalidDependencyIdentifier {
alias: alias.to_string(),
})
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedUse {
pub(crate) path: CanonPath,
#[allow(dead_code)]
pub(crate) mod_path: Vec<Identifier>,
}
impl DependencyMap {
pub fn get_package_root(&self, current_file: &CanonPath) -> Option<&CanonPath> {
self.package_roots
.iter()
.find(|root| current_file.starts_with(root))
}
pub fn resolve_path(
&self,
current_file: &CanonPath,
use_decl: &UseDecl,
) -> Result<CanonPath, RichError> {
Ok(self.resolve_path_internal(current_file, use_decl)?.path)
}
pub(crate) fn resolve_path_internal(
&self,
current_file: &CanonPath,
use_decl: &UseDecl,
) -> Result<ResolvedUse, RichError> {
let drp_name = use_decl.drp_name()?;
let span = *use_decl.span();
if drp_name == CRATE_STR {
return self.resolve_crate_path(current_file, use_decl);
}
self.remappings
.iter()
.find(|r| current_file.starts_with(&r.context_prefix) && r.drp_name == drp_name)
.ok_or_else(|| {
RichError::new(
Error::UnknownLibrary {
name: drp_name.to_string(),
},
span,
)
})
.and_then(|remapping| self.resolve_external_path(remapping, current_file, use_decl))
}
fn resolve_external_path(
&self,
remapping: &Remapping,
current_file: &CanonPath,
use_decl: &UseDecl,
) -> Result<ResolvedUse, RichError> {
let drp_name = use_decl.drp_name()?;
let parts_without_drp_name = &use_decl.path()[1..];
let resolved = Self::build_and_verify_path(&remapping.target, parts_without_drp_name)
.map_err(|failed_path| {
RichError::new(
Error::ExternalFileNotFound {
lib: drp_name.to_string(),
filename: failed_path,
},
*use_decl.span(),
)
})?;
self.check_local_file_imported_as_external(current_file, &resolved.path, use_decl.span())?;
Ok(resolved)
}
fn resolve_crate_path(
&self,
current_file: &CanonPath,
use_decl: &UseDecl,
) -> Result<ResolvedUse, RichError> {
let root = self
.get_package_root(current_file)
.ok_or_else(|| Error::Internal {
msg: "The 'crate' root path was not configured by the compiler.".to_string(),
})
.map_err(|e| RichError::new(e, *use_decl.span()))?;
let parts_without_drp_name = &use_decl.path()[1..];
let failed_path = match Self::build_and_verify_path(root, parts_without_drp_name) {
Ok(resolved) => return Ok(resolved),
Err(path) => path,
};
let is_in_root_dir = current_file.as_path().parent() == Some(root.as_path());
if is_in_root_dir {
return Ok(ResolvedUse {
path: current_file.clone(),
mod_path: parts_without_drp_name.to_vec(),
});
}
Err(RichError::new(
Error::FileNotFound {
filename: failed_path,
},
*use_decl.span(),
))
}
fn check_local_file_imported_as_external(
&self,
current_file: &CanonPath,
resolved: &CanonPath,
use_decl_span: &crate::error::Span,
) -> Result<(), RichError> {
if let (Some(curr), Some(res)) = (
self.get_package_root(current_file),
self.get_package_root(resolved),
) {
if curr == res {
return Err(Error::LocalFileImportedAsExternal {
path: resolved.as_path().to_path_buf(),
})
.with_span(*use_decl_span);
}
}
Ok(())
}
fn build_and_verify_path(
base_target: &CanonPath,
module_parts: &[Identifier],
) -> Result<ResolvedUse, std::path::PathBuf> {
let mut path = base_target.as_path().to_path_buf();
let mut iter = module_parts.iter();
while let Some(part) = iter.next() {
let joined = path.join(part.as_inner());
if joined.is_dir() {
path = joined;
continue;
}
let mut file_candidate = joined;
file_candidate.set_extension("simf");
let resolved =
CanonPath::canonicalize(&file_candidate).map_err(|_| file_candidate.clone())?;
if !resolved.starts_with(base_target) {
return Err(file_candidate);
}
return Ok(ResolvedUse {
path: resolved,
mod_path: iter.cloned().collect(), });
}
Err(path)
}
}
#[cfg(test)]
pub(crate) mod tests {
use crate::str::Identifier;
use crate::test_utils::TempWorkspace;
use std::path::Path;
use super::*;
pub fn canon(p: &Path) -> CanonPath {
CanonPath::canonicalize(p).unwrap_or_else(|_| CanonPath::dummy_for_test(p))
}
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)
}
fn dirs<const N: usize>(ws: &TempWorkspace, paths: [&str; N]) -> [CanonPath; N] {
paths.map(|p| canon(&ws.create_dir(p)))
}
fn files<const N: usize>(ws: &TempWorkspace, paths: [&str; N]) -> [CanonPath; N] {
paths.map(|p| canon(&ws.create_file(p, "")))
}
pub(crate) fn build_map(
root: &CanonPath,
deps: &[(&CanonPath, &str, &CanonPath)],
) -> Result<DependencyMap, Error> {
let mut builder = DependencyMapBuilder::new();
for (ctx, alias, target) in deps {
builder.add_dependency((*ctx).clone(), alias.to_string(), (*target).clone());
}
builder.build(root.clone())
}
#[test]
fn test_insert_crate_fails() {
let ws = TempWorkspace::new("insert_crate_fail");
let [project_dir] = dirs(&ws, ["workspace"]);
let result = build_map(&project_dir, &[(&project_dir, CRATE_STR, &project_dir)]);
assert!(matches!(
result.unwrap_err(),
Error::ReservedDependencyKeyword { .. }
));
}
#[test]
fn test_sorting_longest_prefix() {
let ws = TempWorkspace::new("sorting");
let [workspace_dir, project_a_dir, nested_dir, target_v1, target_v2, target_v3] = dirs(
&ws,
[
"workspace",
"workspace/project_a",
"workspace/project_a/nested",
"lib/math_v1",
"lib/math_v2",
"lib/math_v3",
],
);
let map = build_map(
&workspace_dir,
&[
(&workspace_dir, "math", &target_v1),
(&nested_dir, "math", &target_v3),
(&project_a_dir, "math", &target_v2),
],
)
.unwrap();
assert_eq!(map.remappings[0].context_prefix, nested_dir);
assert_eq!(map.remappings[1].context_prefix, project_a_dir);
assert_eq!(map.remappings[2].context_prefix, workspace_dir);
}
#[test]
fn test_context_isolation() {
let ws = TempWorkspace::new("isolation");
let [project_a, target_utils] = dirs(&ws, ["project_a", "libs/utils_a"]);
let [current_file] = files(&ws, ["project_b/main.simf"]);
let map = build_map(&project_a, &[(&project_a, "utils", &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, global_target, frontend_context, frontend_target] = dirs(
&ws,
[
"workspace",
"libs/global_math",
"workspace/frontend",
"libs/frontend_math",
],
);
let [global_expected, frontend_expected, frontend_file, backend_file] = files(
&ws,
[
"libs/global_math/vector.simf",
"libs/frontend_math/vector.simf",
"workspace/frontend/src/main.simf",
"workspace/backend/src/main.simf",
],
);
let map = build_map(
&global_context,
&[
(&global_context, "math", &global_target),
(&frontend_context, "math", &frontend_target),
],
)
.unwrap();
let use_decl = create_dummy_use_decl(&["math", "vector"]);
assert_eq!(
map.resolve_path(&frontend_file, &use_decl).unwrap(),
frontend_expected
);
assert_eq!(
map.resolve_path(&backend_file, &use_decl).unwrap(),
global_expected
);
}
#[test]
fn test_local_file_imported_as_external() {
let ws = TempWorkspace::new("local_as_ext");
let [project_dir] = dirs(&ws, ["workspace"]);
let [_utils_file, current_file] =
files(&ws, ["workspace/utils.simf", "workspace/main.simf"]);
let map = build_map(&project_dir, &[(&project_dir, "utils_lib", &project_dir)]).unwrap();
let use_decl = create_dummy_use_decl(&["utils_lib", "utils"]);
let result = map.resolve_path(¤t_file, &use_decl);
assert!(matches!(
result.unwrap_err().error(),
Error::LocalFileImportedAsExternal { .. }
));
}
#[test]
fn test_crate_resolution() {
let ws = TempWorkspace::new("crate_res");
let [project_dir] = dirs(&ws, ["workspace"]);
let [expected, current_file] = files(&ws, ["workspace/utils.simf", "workspace/main.simf"]);
let map = build_map(&project_dir, &[]).unwrap();
let use_decl = create_dummy_use_decl(&[CRATE_STR, "utils"]);
let result = map.resolve_path(¤t_file, &use_decl).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_crate_unconfigured_error() {
let ws = TempWorkspace::new("crate_unconf");
let [other_dir] = dirs(&ws, ["other_dir"]);
let [current_file] = files(&ws, ["workspace/main.simf"]);
let map = build_map(&other_dir, &[]).unwrap();
let use_decl = create_dummy_use_decl(&[CRATE_STR, "utils"]);
let result = map.resolve_path(¤t_file, &use_decl);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().error(),
Error::Internal{msg} if msg.contains("The 'crate' root path was not configured")
));
}
#[test]
fn test_resolve_relative_current_file_against_canonical_context() {
let ws = TempWorkspace::new("relative_current");
let [context, target] = dirs(&ws, ["workspace/frontend", "libs/frontend_math"]);
let [expected, current_file] = files(
&ws,
[
"libs/frontend_math/vector.simf",
"workspace/frontend/src/main.simf",
],
);
let map = build_map(&context, &[(&context, "math", &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);
}
#[test]
fn test_builder_rejects_file_as_directory() {
let ws = TempWorkspace::new("file_as_dir");
let [valid_dir] = dirs(&ws, ["workspace/valid_dir"]);
let [file_path] = files(&ws, ["workspace/not_a_dir.simf"]);
let res1 = build_map(&file_path, &[]);
assert!(matches!(
res1.unwrap_err(),
Error::DependencyNotADirectory { .. }
));
let res2 = build_map(&valid_dir, &[(&file_path, "alias", &valid_dir)]);
assert!(matches!(
res2.unwrap_err(),
Error::DependencyNotADirectory { .. }
));
let res3 = build_map(&valid_dir, &[(&valid_dir, "alias", &file_path)]);
assert!(matches!(
res3.unwrap_err(),
Error::DependencyNotADirectory { .. }
));
}
#[test]
fn test_builder_rejects_non_existent_paths() {
let ws = TempWorkspace::new("non_existent");
let [valid_dir] = dirs(&ws, ["workspace/valid_dir"]);
let fake_path = CanonPath::dummy_for_test(Path::new("/does/not/exist/in/this/universe"));
let res = build_map(&valid_dir, &[(&valid_dir, "alias", &fake_path)]);
assert!(matches!(
res.unwrap_err(),
Error::DependencyPathNotFound { .. }
));
}
#[test]
fn test_builder_rejects_invalid_identifiers() {
let ws = TempWorkspace::new("invalid_idents");
let [valid_dir] = dirs(&ws, ["workspace/valid_dir"]);
let bad_aliases = vec!["", "123lib", "my-lib", "lib!", " space "];
for bad_alias in bad_aliases {
let res = build_map(&valid_dir, &[(&valid_dir, bad_alias, &valid_dir)]);
assert!(
matches!(res.unwrap_err(), Error::InvalidDependencyIdentifier { .. }),
"Builder should reject alias: '{}'",
bad_alias
);
}
}
#[test]
fn test_builder_rejects_reserved_keywords() {
let ws = TempWorkspace::new("reserved_keywords");
let [valid_dir] = dirs(&ws, ["workspace/valid_dir"]);
for kw in crate::lexer::KEYWORDS.to_vec() {
let res = build_map(&valid_dir, &[(&valid_dir, kw, &valid_dir)]);
let err = res.unwrap_err();
if kw == CRATE_STR {
assert!(matches!(err, Error::ReservedDependencyKeyword { .. }));
} else {
assert!(matches!(err, Error::InvalidDependencyIdentifier { .. }));
}
}
}
#[test]
fn test_builder_rejects_duplicates() {
let ws = TempWorkspace::new("duplicates");
let [valid_dir, target1, target2] = dirs(
&ws,
[
"workspace/valid_dir",
"workspace/target1",
"workspace/target2",
],
);
let res = build_map(
&valid_dir,
&[
(&valid_dir, "alias", &target1),
(&valid_dir, "alias", &target2),
],
);
assert!(matches!(
res.unwrap_err(),
Error::DuplicateDependencyAlias { .. }
));
}
#[test]
fn test_resolve_rejects_escaping_package_root() {
let ws = TempWorkspace::new("escaping_root");
let [context, target] = dirs(&ws, ["workspace", "libs/target"]);
let [current_file, _outside_file] =
files(&ws, ["workspace/main.simf", "libs/escaped.simf"]);
let map = build_map(&context, &[(&context, "alias", &target)]).unwrap();
let use_decl = create_dummy_use_decl(&["alias", "..", "escaped"]);
let result = map.resolve_path(¤t_file, &use_decl);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[cfg(unix)]
#[test]
fn test_dependency_symlink_escape_rejected() {
let ws = TempWorkspace::new("dependency_symlink_escape");
let [workspace_dir] = dirs(&ws, ["workspace"]);
let dependency_dir_path = ws.create_dir("deps/package");
let escaped_file = ws.create_file("outside/foo.simf", "");
std::os::unix::fs::symlink(&escaped_file, dependency_dir_path.join("foo.simf")).unwrap();
let dependency_dir = canon(&dependency_dir_path);
let [current_file] = files(&ws, ["workspace/main.simf"]);
let map = build_map(&workspace_dir, &[(&workspace_dir, "dep", &dependency_dir)]).unwrap();
let use_decl = create_dummy_use_decl(&["dep", "foo"]);
map.resolve_path(¤t_file, &use_decl)
.expect_err("dependency symlink escape was accepted");
}
#[test]
fn test_package_roots_deduplication() {
let ws = TempWorkspace::new("dedup_roots");
let [workspace_dir, lib_a, lib_b] =
dirs(&ws, ["workspace", "workspace/libs/A", "workspace/libs/B"]);
let map = build_map(
&workspace_dir,
&[
(&workspace_dir, "lib_a", &lib_a),
(&workspace_dir, "lib_b", &lib_b),
(&lib_b, "lib_a", &lib_a),
],
)
.unwrap();
assert_eq!(
map.package_roots.len(),
3,
"Package roots were not correctly deduplicated"
);
}
#[test]
fn test_crate_resolves_to_closest_package_root() {
let ws = TempWorkspace::new("closest_root");
let [workspace_dir, lib_dir] = dirs(&ws, ["workspace", "workspace/libs/math"]);
let [lib_file] = files(&ws, ["workspace/libs/math/vector.simf"]);
let map = build_map(&workspace_dir, &[(&workspace_dir, "math", &lib_dir)]).unwrap();
let lib_crate = map.get_package_root(&lib_file).unwrap();
assert_eq!(
lib_crate, &lib_dir,
"Nested dependency did not securely shadow the parent workspace root"
);
}
}