diff --git a/packages/@winglang/wingc/src/lib.rs b/packages/@winglang/wingc/src/lib.rs index 75a2fec4932..193091d3ba5 100644 --- a/packages/@winglang/wingc/src/lib.rs +++ b/packages/@winglang/wingc/src/lib.rs @@ -8,7 +8,7 @@ extern crate lazy_static; use ast::{Scope, Symbol}; -use camino::{Utf8Path, Utf8PathBuf}; +use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; use closure_transform::ClosureTransformer; use comp_ctx::set_custom_panic_hook; use const_format::formatcp; @@ -328,6 +328,83 @@ pub fn find_nearest_wing_project_dir(source_path: &Utf8Path) -> Utf8PathBuf { return initial_dir; } +/// Calculate the common path of two Utf8Path objects. +/// +/// This function takes two Utf8Path objects and returns the common path of the two. +/// If one path is a parent directory of the other, it returns the shorter path. +/// If the paths are not related, it returns the common prefix of the two paths. +/// +/// Paths should be normalized before calling this function, so that there aren't +/// oddities like "./foo/../bar" etc. +/// +/// ``` +/// use camino::Utf8Path; +/// use wingc::common_path; +/// +/// let path = common_path("/home/user/project".into(), "/home/user/project/src/main.w".into()); +/// assert_eq!(path, Utf8Path::new("/home/user/project")); +/// +/// let path = common_path("/home/user/project".into(), "/home/user/other".into()); +/// assert_eq!(path, Utf8Path::new("/home/user")); +/// +/// let path = common_path("./foo".into(), "./bar".into()); +/// assert_eq!(path, Utf8Path::new(".")); +/// +/// let path = common_path("../foo/bar".into(), "../foo/baz".into()); +/// assert_eq!(path, Utf8Path::new("../foo")); +/// +/// let path = common_path("../foo".into(), "../../bar".into()); +/// assert_eq!(path, Utf8Path::new("../../")); +/// ``` +pub fn common_path(path1: &Utf8Path, path2: &Utf8Path) -> Utf8PathBuf { + let components1: Vec<_> = path1.components().collect(); + let components2: Vec<_> = path2.components().collect(); + + if path1.is_absolute() && path2.is_absolute() { + // Both paths are absolute + components1 + .iter() + .zip(components2.iter()) + .take_while(|&(a, b)| a == b) + .map(|(component, _)| component) + .collect() + } else if !path1.is_absolute() && !path2.is_absolute() { + // Both paths are relative + let mut common = Vec::new(); + let mut iter1 = components1.iter().peekable(); + let mut iter2 = components2.iter().peekable(); + + // Skip common prefix of parent directory components + while iter1.peek() == Some(&&Utf8Component::ParentDir) && iter2.peek() == Some(&&Utf8Component::ParentDir) { + common.push(iter1.next().unwrap()); + iter2.next(); + } + + // If one of the paths is a further parent directory, then we should return that + if iter1.peek() == Some(&&Utf8Component::ParentDir) { + common.push(iter1.next().unwrap()); + return common.into_iter().collect(); + } + + if iter2.peek() == Some(&&Utf8Component::ParentDir) { + common.push(iter2.next().unwrap()); + return common.into_iter().collect(); + } + + // Find common path after parent directory components + common.extend( + iter1 + .zip(iter2) + .take_while(|&(a, b)| a == b) + .map(|(component, _)| component), + ); + + common.into_iter().collect() + } else { + panic!("path1 and path2 must be either both absolute or both relative"); + } +} + pub fn compile(source_path: &Utf8Path, source_text: Option, out_dir: &Utf8Path) -> Result { let project_dir = find_nearest_wing_project_dir(source_path); let source_package = as_wing_library(&project_dir, false).unwrap_or_else(|| DEFAULT_PACKAGE_NAME.to_string()); @@ -336,7 +413,7 @@ pub fn compile(source_path: &Utf8Path, source_text: Option, out_dir: &Ut // A map from package names to their root directories let mut library_roots: IndexMap = IndexMap::new(); - library_roots.insert(source_package, project_dir.to_owned()); + library_roots.insert(source_package.clone(), project_dir.to_owned()); // -- PARSING PHASE -- let mut files = Files::new(); @@ -353,6 +430,22 @@ pub fn compile(source_path: &Utf8Path, source_text: Option, out_dir: &Ut &mut asts, ); + // If there's no wing.toml or package.json, then we don't know for sure where the root + // of the project is, so we simply find the common root of all files that are part of the + // default package. + if source_package == DEFAULT_PACKAGE_NAME { + let mut common_root = project_dir.to_owned(); + for file in &topo_sorted_files { + if file.package != source_package { + continue; + } + + common_root = common_path(&common_root, &file.path); + } + // Update the source package to be the common root + library_roots.insert(source_package.clone(), common_root); + } + emit_warning_for_unsupported_package_managers(&project_dir); // -- DESUGARING PHASE -- diff --git a/packages/@winglang/wingc/src/type_check.rs b/packages/@winglang/wingc/src/type_check.rs index fca22eb373c..57c9a8bee83 100644 --- a/packages/@winglang/wingc/src/type_check.rs +++ b/packages/@winglang/wingc/src/type_check.rs @@ -7496,27 +7496,58 @@ fn lookup_known_type(name: &'static str, env: &SymbolEnv) -> TypeRef { /// ``` pub fn calculate_fqn_for_namespace(package_name: &str, package_root: &Utf8Path, path: &Utf8Path) -> String { let normalized_root = normalize_path(&package_root, None); - let normalized = normalize_path(&path, None); - if normalized.starts_with("..") { - panic!( - "File path \"{}\" is not within the package root \"{}\"", - path, package_root - ); + let normalized_path = normalize_path(&path, None); + if normalized_path.starts_with("..") { + report_diagnostic(Diagnostic { + message: format!( + "File path \"{}\" is not within the package root \"{}\"", + path, package_root + ), + span: None, + annotations: vec![], + hints: vec![], + severity: DiagnosticSeverity::Error, + }); + return package_name.to_string(); } + let assembly = package_name; - let normalized = if normalized.as_str().ends_with(".w") { - normalized.parent().expect("Expected a parent directory") + let normalized_dir = if normalized_path.as_str().ends_with(".w") { + if let Some(normalized_parent) = normalized_path.parent() { + normalized_parent + } else { + report_diagnostic(Diagnostic { + message: format!("Source file \"{}\" does not have a parent directory", path), + span: None, + annotations: vec![], + hints: vec![], + severity: DiagnosticSeverity::Error, + }); + return package_name.to_string(); + } } else { - &normalized + &normalized_path }; - let relative_path = normalized - .strip_prefix(&normalized_root) - .expect(format!("not a prefix: {} {}", normalized_root, normalized).as_str()); - if relative_path == Utf8Path::new("") { - return assembly.to_string(); - } - let namespace = relative_path.as_str().replace("/", "."); - format!("{}.{}", assembly, namespace) + + if let Ok(relative_path) = normalized_dir.strip_prefix(&normalized_root) { + if relative_path == Utf8Path::new("") { + return assembly.to_string(); + } + let namespace = relative_path.as_str().replace("/", "."); + return format!("{}.{}", assembly, namespace); + } else { + report_diagnostic(Diagnostic { + message: format!( + "Source file \"{}\" is not within the package root \"{}\"", + path, package_root + ), + span: None, + annotations: vec![], + hints: vec![], + severity: DiagnosticSeverity::Error, + }); + return package_name.to_string(); + } } #[derive(Debug)] diff --git a/tests/valid/subdir/bring_outer.main.w b/tests/valid/subdir/bring_outer.main.w new file mode 100644 index 00000000000..c48e2265507 --- /dev/null +++ b/tests/valid/subdir/bring_outer.main.w @@ -0,0 +1 @@ +bring "../baz.w" as baz;