diff --git a/src/disk.rs b/src/disk.rs index 1fa35a72..b969d47d 100644 --- a/src/disk.rs +++ b/src/disk.rs @@ -167,7 +167,7 @@ pub struct Disk { } impl Disk { - pub fn from_workspace(io: &T, directory: &Path) -> Self { + pub fn from_workspace(io: &T, directory: &Path) -> Result { let lerna_json = io.read_json_file(&directory.join("lerna.json")); let package_json_root_file = io.read_json_file(&directory.join("package.json")); let package_manager = if io.path_exists(&directory.join("pnpm-lock.yaml")) || io.path_exists(&directory.join("pnpm-workspace.yaml")) { @@ -182,7 +182,7 @@ impl Disk { Some(PackageManager::Unknown) }; let pnpm_workspace = if let Some(PackageManager::Pnpm) = package_manager { - io.read_yaml_file(&directory.join("pnpm-workspace.yaml")) + io.read_yaml_file(&directory.join("pnpm-workspace.yaml")).transpose()? } else { None }; @@ -194,14 +194,14 @@ impl Disk { package_json_files.push(root_file); } - Self { + Ok(Self { cwd: directory.to_path_buf(), lerna_json: lerna_json.and_then(Result::ok), package_json_files, package_json_root_idx, package_manager, - pnpm_workspace: pnpm_workspace.and_then(Result::ok), - } + pnpm_workspace, + }) } /// Borrow the root package.json if one was loaded. diff --git a/src/disk_test.rs b/src/disk_test.rs index 38f27dea..e82be20b 100644 --- a/src/disk_test.rs +++ b/src/disk_test.rs @@ -1,8 +1,8 @@ use { crate::{ disk::{ - Disk, File, PackageManager, PendingYamlOp, empty_yaml_file, ensure_object_path, insert_catalog_definition, json_view, - parse_yaml_file, remove_catalog_definition, remove_prop, render_yaml_bytes, set_nested_prop, set_prop, + Disk, DiskIoError, File, PackageManager, PendingYamlOp, empty_yaml_file, ensure_object_path, insert_catalog_definition, json_view, + parse_yaml_file, parse_yaml_file_strict, remove_catalog_definition, remove_prop, render_yaml_bytes, set_nested_prop, set_prop, }, specifier::Specifier, test::mock_disk::MockDiskIo, @@ -24,7 +24,7 @@ fn json_file_from_value(value: serde_json::Value) -> File { fn detects_pnpm_from_workspace_yaml_without_lockfile() { let mut io = MockDiskIo::new(); io.add_file("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n".to_string()); - let disk = Disk::from_workspace(&io, io.root()); + let disk = Disk::from_workspace(&io, io.root()).unwrap(); assert_eq!(disk.package_manager, Some(PackageManager::Pnpm)); assert!(disk.pnpm_workspace.is_some(), "pnpm-workspace.yaml should be loaded"); } @@ -33,7 +33,7 @@ fn detects_pnpm_from_workspace_yaml_without_lockfile() { fn detects_bun_from_legacy_binary_lockfile() { let mut io = MockDiskIo::new(); io.add_file("bun.lockb", String::new()); - let disk = Disk::from_workspace(&io, io.root()); + let disk = Disk::from_workspace(&io, io.root()).unwrap(); assert_eq!(disk.package_manager, Some(PackageManager::Bun)); } @@ -41,7 +41,7 @@ fn detects_bun_from_legacy_binary_lockfile() { fn file_dirty_defaults_false() { let mut io = MockDiskIo::new(); io.add_file("package.json", "{\"name\":\"root\"}".to_string()); - let disk = Disk::from_workspace(&io, io.root()); + let disk = Disk::from_workspace(&io, io.root()).unwrap(); let pkg = disk.package_json_root().expect("root should be loaded"); assert!(!pkg.dirty, "freshly parsed File should default dirty=false"); assert!(!pkg.is_dirty(), "is_dirty() should match field"); @@ -65,7 +65,7 @@ fn file_dirty_settable() { fn disk_from_workspace_works_without_lifetime() { // Disk owns no borrow of `io`; returned Disk has no lifetime parameter. let io = MockDiskIo::new(); - let disk: Disk = Disk::from_workspace(&io, io.root()); + let disk: Disk = Disk::from_workspace(&io, io.root()).unwrap(); // Drop io to prove Disk does not borrow it. drop(io); assert!(disk.package_json_root().is_none()); @@ -90,7 +90,7 @@ fn disk_constructs_without_io_field() { fn disk_root_idx_set_when_root_pkg_exists() { let mut io = MockDiskIo::new(); io.add_file("package.json", "{\"name\":\"root\"}".to_string()); - let disk = Disk::from_workspace(&io, io.root()); + let disk = Disk::from_workspace(&io, io.root()).unwrap(); assert_eq!(disk.package_json_root_idx, Some(0), "root_idx should be Some(0) when root exists"); assert_eq!(disk.package_json_files.len(), 1, "root pkg should be in package_json_files"); let root = disk.package_json_root().expect("root accessor returns Some"); @@ -107,7 +107,7 @@ fn disk_load_package_files_appends_after_root() { io.add_file("package.json", "{\"name\":\"root\"}".to_string()); io.add_file("packages/a/package.json", "{\"name\":\"a\"}".to_string()); io.add_file("packages/b/package.json", "{\"name\":\"b\"}".to_string()); - let mut disk = Disk::from_workspace(&io, io.root()); + let mut disk = Disk::from_workspace(&io, io.root()).unwrap(); assert_eq!(disk.package_json_files.len(), 1, "starts with just root"); let a_path = io.root().join("packages/a/package.json"); let b_path = io.root().join("packages/b/package.json"); @@ -120,7 +120,7 @@ fn disk_load_package_files_skips_root_path() { let mut io = MockDiskIo::new(); io.add_file("package.json", "{\"name\":\"root\"}".to_string()); io.add_file("packages/a/package.json", "{\"name\":\"a\"}".to_string()); - let mut disk = Disk::from_workspace(&io, io.root()); + let mut disk = Disk::from_workspace(&io, io.root()).unwrap(); let root_path = io.root().join("package.json"); let a_path = io.root().join("packages/a/package.json"); disk.load_package_files(&io, &[root_path, a_path]); @@ -650,6 +650,26 @@ fn parse_yaml_file_captures_raw_text() { assert!(yaml.patches.is_empty(), "fresh YamlFile starts with no pending patches"); } +#[test] +fn parse_yaml_file_strict_returns_error_for_invalid_yaml() { + let filepath = PathBuf::from("/test/pnpm-workspace.yaml"); + let err = parse_yaml_file_strict("catalogs:\n tooling: [\n".to_string(), filepath).unwrap_err(); + let DiskIoError::YamlParse(source) = &err else { + panic!("invalid YAML must return a YAML parse error"); + }; + let location = source.location().expect("syntax error should have a location"); + let message = err.to_string(); + assert!( + message.contains(&source.to_string()), + "error message should include the parser's reason" + ); + assert!(message.contains(&format!("line {} column {}", location.line(), location.column()))); + assert!( + std::error::Error::source(&err).is_some(), + "original parser error should remain in the error chain" + ); +} + #[test] fn empty_yaml_file_has_empty_raw() { let yaml = empty_yaml_file(PathBuf::from("/test/pnpm-workspace.yaml")); diff --git a/src/syncpack.rs b/src/syncpack.rs index 613478b4..e0e8bd43 100644 --- a/src/syncpack.rs +++ b/src/syncpack.rs @@ -38,7 +38,7 @@ pub async fn syncpack( /// All disk reads happen here. fn analyse(cli: Cli, io: &D) -> Result { logger::configure(&cli); - let mut disk = Disk::from_workspace(io, &cli.cwd); + let mut disk = Disk::from_workspace(io, &cli.cwd)?; let rcfile = Rcfile::from_disk(&disk, io, &cli).map_err(SyncpackError::RcfileError)?; let config = Config { cli, diff --git a/src/version_group/bun_catalog_test.rs b/src/version_group/bun_catalog_test.rs index 452ef295..0133304a 100644 --- a/src/version_group/bun_catalog_test.rs +++ b/src/version_group/bun_catalog_test.rs @@ -189,7 +189,7 @@ async fn bun_dual_path_aborts() { }), ); io.add_file("bun.lock", "{}".to_string()); - let disk = Disk::from_workspace(&io, std::env::current_dir().unwrap().as_path()); + let disk = Disk::from_workspace(&io, std::env::current_dir().unwrap().as_path()).unwrap(); assert_eq!(disk.package_manager, Some(PackageManager::Bun)); let err = catalogs::make_catalog_dep_types(&disk).unwrap_err(); assert!(matches!(err, SyncpackError::BunDualCatalogPath));