use crate::ir::ast::{ClassDefinition, StoredDefinition};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::sync::Arc;
const IGNORED_DIRECTORIES: &[&str] = &[
".git",
".hg",
".svn",
"target",
"build",
"out",
"dist",
"_build",
"cmake-build-debug",
"cmake-build-release",
"node_modules",
".npm",
"vendor",
"venv",
".venv",
"env",
".env",
"__pycache__",
".tox",
".idea",
".vscode",
".vs",
".cargo",
".eggs",
".mypy_cache",
".pytest_cache",
".cache",
".tmp",
"tmp",
"temp",
".DS_Store",
];
pub fn should_ignore_directory(path: &Path) -> bool {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if IGNORED_DIRECTORIES.contains(&name) {
return true;
}
if name.starts_with('.') && name != "." && name != ".." {
return true;
}
}
false
}
pub fn merge_stored_definitions(
definitions: Vec<(String, StoredDefinition)>,
) -> Result<StoredDefinition> {
let mut merged = StoredDefinition::default();
for (file_path, def) in definitions {
merge_single_definition(&mut merged, def, &file_path)?;
}
Ok(merged)
}
pub fn merge_with_arc_libraries(
libraries: &[Arc<StoredDefinition>],
user_source: StoredDefinition,
source_path: &str,
) -> Result<StoredDefinition> {
let mut merged = StoredDefinition::default();
for lib in libraries {
for (class_name, class_def) in &lib.class_list {
if merged.class_list.contains_key(class_name) {
let existing = merged.class_list.get_mut(class_name).unwrap();
if matches!(existing.class_type, crate::ir::ast::ClassType::Package)
&& matches!(class_def.class_type, crate::ir::ast::ClassType::Package)
{
merge_package_contents(existing, class_def.clone())?;
}
} else {
merged
.class_list
.insert(class_name.clone(), class_def.clone());
}
}
}
merge_single_definition(&mut merged, user_source, source_path)?;
Ok(merged)
}
fn merge_single_definition(
merged: &mut StoredDefinition,
def: StoredDefinition,
file_path: &str,
) -> Result<()> {
let prefix = def
.within
.as_ref()
.map(|n| n.to_string())
.unwrap_or_default();
for (class_name, class_def) in def.class_list {
if prefix.is_empty() {
if merged.class_list.contains_key(&class_name) {
let existing = merged.class_list.get_mut(&class_name).unwrap();
if matches!(existing.class_type, crate::ir::ast::ClassType::Package)
&& matches!(class_def.class_type, crate::ir::ast::ClassType::Package)
{
merge_package_contents(existing, class_def)?;
} else {
anyhow::bail!(
"Duplicate class '{}' found in '{}' (already defined)",
class_name,
file_path
);
}
} else {
merged.class_list.insert(class_name, class_def);
}
} else {
place_class_in_hierarchy(merged, &prefix, class_name, class_def, file_path)?;
}
}
Ok(())
}
fn place_class_in_hierarchy(
merged: &mut StoredDefinition,
prefix: &str,
class_name: String,
class_def: ClassDefinition,
file_path: &str,
) -> Result<()> {
let parts: Vec<&str> = prefix.split('.').collect();
let mut current_map = &mut merged.class_list;
let mut current_path = String::new();
for (i, part) in parts.iter().enumerate() {
if !current_path.is_empty() {
current_path.push('.');
}
current_path.push_str(part);
if i == 0 {
if !current_map.contains_key(*part) {
let pkg = ClassDefinition {
name: crate::ir::ast::Token {
text: part.to_string(),
..Default::default()
},
class_type: crate::ir::ast::ClassType::Package,
..Default::default()
};
current_map.insert(part.to_string(), pkg);
}
let pkg = current_map.get_mut(*part).with_context(|| {
format!(
"Failed to get package '{}' when placing class from '{}'",
part, file_path
)
})?;
current_map = &mut pkg.classes;
} else {
if !current_map.contains_key(*part) {
let pkg = ClassDefinition {
name: crate::ir::ast::Token {
text: part.to_string(),
..Default::default()
},
class_type: crate::ir::ast::ClassType::Package,
..Default::default()
};
current_map.insert(part.to_string(), pkg);
}
let pkg = current_map.get_mut(*part).with_context(|| {
format!(
"Failed to get nested package '{}' when placing class from '{}'",
part, file_path
)
})?;
current_map = &mut pkg.classes;
}
}
if current_map.contains_key(&class_name) {
let existing = current_map.get_mut(&class_name).unwrap();
if matches!(existing.class_type, crate::ir::ast::ClassType::Package)
&& matches!(class_def.class_type, crate::ir::ast::ClassType::Package)
{
merge_package_contents(existing, class_def)?;
} else {
anyhow::bail!(
"Duplicate class '{}.{}' found in '{}' (already defined)",
prefix,
class_name,
file_path
);
}
} else {
current_map.insert(class_name, class_def);
}
Ok(())
}
fn merge_package_contents(existing: &mut ClassDefinition, new: ClassDefinition) -> Result<()> {
for (name, class) in new.classes {
if existing.classes.contains_key(&name) {
let existing_nested = existing.classes.get_mut(&name).unwrap();
if matches!(
existing_nested.class_type,
crate::ir::ast::ClassType::Package
) && matches!(class.class_type, crate::ir::ast::ClassType::Package)
{
merge_package_contents(existing_nested, class)?;
} else {
anyhow::bail!("Duplicate class '{}' in package", name);
}
} else {
existing.classes.insert(name, class);
}
}
for (name, comp) in new.components {
if existing.components.contains_key(&name) {
anyhow::bail!("Duplicate component '{}' in package", name);
}
existing.components.insert(name, comp);
}
existing.extends.extend(new.extends);
existing.imports.extend(new.imports);
Ok(())
}
pub fn find_modelica_files(search_paths: &[std::path::PathBuf]) -> Result<Vec<std::path::PathBuf>> {
let mut files = Vec::new();
for path in search_paths {
if path.is_file() && path.extension().is_some_and(|e| e == "mo") {
files.push(path.clone());
} else if path.is_dir() {
find_files_recursive(path, &mut files)?;
}
}
Ok(files)
}
fn find_files_recursive(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) -> Result<()> {
if should_ignore_directory(dir) {
return Ok(());
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|e| e == "mo") {
files.push(path);
} else if path.is_dir() && !should_ignore_directory(&path) {
find_files_recursive(&path, files)?;
}
}
Ok(())
}
pub fn package_path_from_file(
file_path: &std::path::Path,
base_dir: &std::path::Path,
) -> Option<String> {
let relative = file_path.strip_prefix(base_dir).ok()?;
let mut parts: Vec<&str> = relative
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
if let Some(last) = parts.last_mut()
&& last.ends_with(".mo")
{
*last = &last[..last.len() - 3];
}
if parts.last() == Some(&"package") {
parts.pop();
}
if parts.is_empty() {
None
} else {
Some(parts.join("."))
}
}
pub fn parse_package_order(path: &Path) -> Result<Vec<String>> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read package.order: {}", path.display()))?;
let mut order = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with("//") {
order.push(trimmed.to_string());
}
}
Ok(order)
}
pub fn is_modelica_package(dir: &Path) -> bool {
dir.is_dir() && dir.join("package.mo").exists()
}
pub fn parse_modelica_path_string(path_str: &str, separator: char) -> Vec<PathBuf> {
path_str
.split(separator)
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect()
}
pub fn get_modelica_path() -> Vec<PathBuf> {
let separator = if cfg!(windows) { ';' } else { ':' };
parse_modelica_path_string(
&std::env::var("MODELICAPATH").unwrap_or_default(),
separator,
)
}
pub fn extract_package_name(dir_name: &str) -> &str {
if let Some(space_idx) = dir_name.rfind(' ') {
let suffix = &dir_name[space_idx + 1..];
if !suffix.is_empty()
&& suffix.chars().next().is_some_and(|c| c.is_ascii_digit())
&& suffix.chars().all(|c| c.is_ascii_digit() || c == '.')
{
return &dir_name[..space_idx];
}
}
dir_name
}
pub fn find_package_in_paths(package_name: &str, search_paths: &[PathBuf]) -> Option<PathBuf> {
for base_path in search_paths {
let dir_path = base_path.join(package_name);
if is_modelica_package(&dir_path) {
return Some(dir_path);
}
let file_path = base_path.join(format!("{}.mo", package_name));
if file_path.is_file() {
return Some(file_path);
}
if let Ok(entries) = std::fs::read_dir(base_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir()
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
if extract_package_name(name) == package_name && is_modelica_package(&path) {
return Some(path);
}
}
}
}
}
None
}
pub fn find_package_in_modelica_path(package_name: &str) -> Option<PathBuf> {
find_package_in_paths(package_name, &get_modelica_path())
}
pub fn discover_package_files(package_dir: &Path) -> Result<Vec<PathBuf>> {
if !is_modelica_package(package_dir) {
anyhow::bail!(
"Directory '{}' is not a Modelica package (missing package.mo)",
package_dir.display()
);
}
let mut files = Vec::new();
let package_mo = package_dir.join("package.mo");
files.push(package_mo);
let order_file = package_dir.join("package.order");
let ordered_names: Option<Vec<String>> = if order_file.exists() {
Some(parse_package_order(&order_file)?)
} else {
None
};
let mut entities: Vec<(String, PathBuf)> = Vec::new();
for entry in std::fs::read_dir(package_dir)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name == "package.mo" || name == "package.order" {
continue;
}
if path.is_file() && name.ends_with(".mo") {
let class_name = name.trim_end_matches(".mo").to_string();
entities.push((class_name, path));
} else if path.is_dir() && is_modelica_package(&path) {
entities.push((name, path));
}
}
if let Some(order) = ordered_names {
let order_map: std::collections::HashMap<&str, usize> = order
.iter()
.enumerate()
.map(|(i, name)| (name.as_str(), i))
.collect();
entities.sort_by(|(a, _), (b, _)| {
let pos_a = order_map.get(a.as_str()).copied().unwrap_or(usize::MAX);
let pos_b = order_map.get(b.as_str()).copied().unwrap_or(usize::MAX);
pos_a.cmp(&pos_b).then_with(|| a.cmp(b))
});
} else {
entities.sort_by(|(a, _), (b, _)| a.cmp(b));
}
for (_, path) in entities {
if path.is_file() {
files.push(path);
} else if path.is_dir() {
let sub_files = discover_package_files(&path)?;
files.extend(sub_files);
}
}
Ok(files)
}
pub fn discover_modelica_files(path: &Path) -> Result<Vec<PathBuf>> {
if path.is_file() {
if path.extension().is_some_and(|e| e == "mo") {
Ok(vec![path.to_path_buf()])
} else {
anyhow::bail!("File '{}' is not a Modelica file (.mo)", path.display());
}
} else if path.is_dir() {
if is_modelica_package(path) {
discover_package_files(path)
} else {
find_modelica_files(&[path.to_path_buf()])
}
} else {
anyhow::bail!("Path '{}' does not exist", path.display());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_empty_definitions() {
let result = merge_stored_definitions(vec![]).unwrap();
assert!(result.class_list.is_empty());
}
#[test]
fn test_merge_single_definition() {
let mut def = StoredDefinition::default();
let class = ClassDefinition {
name: crate::ir::ast::Token {
text: "TestModel".to_string(),
..Default::default()
},
class_type: crate::ir::ast::ClassType::Model,
..Default::default()
};
def.class_list.insert("TestModel".to_string(), class);
let result = merge_stored_definitions(vec![("test.mo".to_string(), def)]).unwrap();
assert!(result.class_list.contains_key("TestModel"));
}
#[test]
fn test_merge_with_within_clause() {
let mut def = StoredDefinition {
within: Some(crate::ir::ast::Name {
name: vec![crate::ir::ast::Token {
text: "MyPackage".to_string(),
..Default::default()
}],
}),
..Default::default()
};
let class = ClassDefinition {
name: crate::ir::ast::Token {
text: "TestModel".to_string(),
..Default::default()
},
class_type: crate::ir::ast::ClassType::Model,
..Default::default()
};
def.class_list.insert("TestModel".to_string(), class);
let result = merge_stored_definitions(vec![("test.mo".to_string(), def)]).unwrap();
assert!(result.class_list.contains_key("MyPackage"));
let pkg = result.class_list.get("MyPackage").unwrap();
assert!(pkg.classes.contains_key("TestModel"));
}
#[test]
fn test_package_path_from_file() {
use std::path::Path;
let base = Path::new("/home/user/models");
let path = Path::new("/home/user/models/MyModel.mo");
assert_eq!(
package_path_from_file(path, base),
Some("MyModel".to_string())
);
let path = Path::new("/home/user/models/MyPackage/MyModel.mo");
assert_eq!(
package_path_from_file(path, base),
Some("MyPackage.MyModel".to_string())
);
let path = Path::new("/home/user/models/MyPackage/package.mo");
assert_eq!(
package_path_from_file(path, base),
Some("MyPackage".to_string())
);
}
#[test]
fn test_parse_package_order() {
use std::io::Write;
let temp_dir = std::env::temp_dir();
let order_path = temp_dir.join("test_package.order");
let mut file = std::fs::File::create(&order_path).unwrap();
writeln!(file, "// This is a comment").unwrap();
writeln!(file, "Types").unwrap();
writeln!(file).unwrap();
writeln!(file, "Functions").unwrap();
writeln!(file, " Examples ").unwrap(); drop(file);
let order = parse_package_order(&order_path).unwrap();
assert_eq!(order, vec!["Types", "Functions", "Examples"]);
std::fs::remove_file(&order_path).ok();
}
#[test]
fn test_is_modelica_package() {
let temp_dir = std::env::temp_dir().join("test_modelica_pkg");
std::fs::create_dir_all(&temp_dir).ok();
assert!(!is_modelica_package(&temp_dir));
std::fs::write(temp_dir.join("package.mo"), "package Test end Test;").unwrap();
assert!(is_modelica_package(&temp_dir));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_parse_modelica_path_empty() {
let paths = parse_modelica_path_string("", ':');
assert!(paths.is_empty());
let paths = parse_modelica_path_string("", ';');
assert!(paths.is_empty());
}
#[test]
fn test_parse_modelica_path_single() {
let paths = parse_modelica_path_string("/path/one", ':');
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], PathBuf::from("/path/one"));
}
#[test]
fn test_parse_modelica_path_multiple_unix() {
let paths = parse_modelica_path_string("/path/one:/path/two:/path/three", ':');
assert_eq!(paths.len(), 3);
assert_eq!(paths[0], PathBuf::from("/path/one"));
assert_eq!(paths[1], PathBuf::from("/path/two"));
assert_eq!(paths[2], PathBuf::from("/path/three"));
}
#[test]
fn test_parse_modelica_path_multiple_windows() {
let paths = parse_modelica_path_string("C:\\path\\one;D:\\path\\two", ';');
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], PathBuf::from("C:\\path\\one"));
assert_eq!(paths[1], PathBuf::from("D:\\path\\two"));
}
#[test]
fn test_parse_modelica_path_trailing_separator() {
let paths = parse_modelica_path_string("/path/one:/path/two:", ':');
assert_eq!(paths.len(), 2);
}
#[test]
fn test_find_package_not_found() {
let result = find_package_in_modelica_path("NonExistentPackage12345");
assert!(result.is_none());
}
#[test]
fn test_extract_package_name_no_version() {
assert_eq!(extract_package_name("Modelica"), "Modelica");
assert_eq!(extract_package_name("MyPackage"), "MyPackage");
assert_eq!(extract_package_name("Some_Package"), "Some_Package");
}
#[test]
fn test_extract_package_name_with_version() {
assert_eq!(extract_package_name("Modelica 4.1.0"), "Modelica");
assert_eq!(extract_package_name("Modelica 3.2.3"), "Modelica");
assert_eq!(
extract_package_name("ModelicaServices 4.0.0"),
"ModelicaServices"
);
assert_eq!(extract_package_name("Complex 4.1.0"), "Complex");
assert_eq!(extract_package_name("MyLibrary 1.0"), "MyLibrary");
assert_eq!(extract_package_name("SomePackage 2.0.0.1"), "SomePackage");
}
#[test]
fn test_extract_package_name_non_version_suffix() {
assert_eq!(extract_package_name("My Package Name"), "My Package Name");
assert_eq!(
extract_package_name("Package With Suffix"),
"Package With Suffix"
);
assert_eq!(extract_package_name("Modelica dev"), "Modelica dev");
assert_eq!(extract_package_name("Package beta1"), "Package beta1");
}
}