Skip to content

Commit f74efe2

Browse files
committed
feat: improve error message for empty package.json files (#793)
## Summary Improves error messages for empty package.json files by returning a clear "File is empty" message instead of the cryptic "EOF while parsing a value at line 1 column 0". ## Changes - Add empty file check in `PackageJson::parse()` for both simd and serde implementations - Return `JSONError` with "File is empty" message before attempting JSON parsing - Update tests to expect new error message - Add comprehensive test suite for various corrupted package.json scenarios ## Error Message Improvement **Before:** ``` EOF while parsing a value at line 1 column 0 ``` **After:** ``` File is empty ``` ## Test Plan - ✅ All existing tests pass (150/150) - ✅ Added new test `test_corrupted_package_json` covering multiple scenarios: - Empty file - Null bytes - Trailing commas - Unclosed braces - Invalid escapes - ✅ Updated `incorrect_description_file_2` test for new error message - ✅ Clippy passes with no warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 1a8a766 commit f74efe2

File tree

5 files changed

+82
-4
lines changed

5 files changed

+82
-4
lines changed

src/package_json/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@ pub use serde::*;
1414
#[cfg(target_endian = "little")]
1515
pub use simd::*;
1616

17-
use std::fmt;
17+
use std::{fmt, path::PathBuf};
18+
19+
use crate::JSONError;
20+
21+
/// Check if JSON content is empty or contains only whitespace
22+
fn check_if_empty(json_bytes: &[u8], path: PathBuf) -> Result<(), JSONError> {
23+
// Check if content is empty or whitespace-only
24+
if json_bytes.iter().all(|&b| b.is_ascii_whitespace()) {
25+
return Err(JSONError { path, message: "File is empty".to_string(), line: 0, column: 0 });
26+
}
27+
Ok(())
28+
}
1829

1930
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2031
pub enum PackageType {

src/package_json/serde.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ impl PackageJson {
228228
json.as_str()
229229
};
230230

231+
// Check if empty after BOM stripping
232+
super::check_if_empty(json_string.as_bytes(), path.clone())?;
233+
231234
// Parse JSON
232235
let value = serde_json::from_str::<Value>(json_string).map_err(|error| JSONError {
233236
path: path.clone(),

src/package_json/simd.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ impl PackageJson {
262262
json_bytes[2] = b' ';
263263
}
264264

265+
// Check if empty after BOM stripping
266+
super::check_if_empty(&json_bytes, path.clone())?;
267+
265268
// Create the self-cell with the JSON bytes and parsed BorrowedValue
266269
let cell = PackageJsonCell::try_new(json_bytes.clone(), |bytes| {
267270
// We need a mutable slice from our owned data

src/tests/incorrect_description_file.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ fn incorrect_description_file_2() {
3131
let resolution = Resolver::default().resolve(f.join("pack2"), ".");
3232
let error = ResolveError::Json(JSONError {
3333
path: f.join("pack2/package.json"),
34-
message: String::from("EOF while parsing a value at line 1 column 0"),
35-
line: 1,
34+
message: String::from("File is empty"),
35+
line: 0,
3636
column: 0,
3737
});
3838
assert_eq!(resolution, Err(error));

src/tests/package_json.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Tests for `Resolution::package_json`.
22
3-
use crate::Resolver;
3+
use crate::{ResolveError, Resolver};
44

55
#[test]
66
fn test() {
@@ -62,3 +62,64 @@ fn package_json_with_symlinks_true() {
6262
let package_json_path = package_json.as_ref().map(|p| &p.path);
6363
assert_eq!(package_json_path, Some(&resolved_package_json_path));
6464
}
65+
66+
#[test]
67+
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
68+
fn test_corrupted_package_json() {
69+
use std::path::Path;
70+
71+
use super::memory_fs::MemoryFS;
72+
use crate::{ResolveOptions, ResolverGeneric};
73+
74+
// Test scenarios for various corrupted package.json files
75+
let scenarios = [
76+
("empty_file", "", "File is empty"),
77+
("null_byte_at_start", "\0", "expected value"),
78+
("json_with_embedded_null", "{\"name\":\0\"test\"}", "expected value"),
79+
("trailing_comma", "{\"name\":\"test\",}", "trailing comma"),
80+
("unclosed_brace", "{\"name\":\"test\"", "EOF while parsing"),
81+
("invalid_escape", "{\"name\":\"test\\x\"}", "escape"),
82+
];
83+
84+
for (name, content, expected_message_contains) in scenarios {
85+
let mut fs = MemoryFS::default();
86+
87+
// Write corrupted package.json
88+
fs.add_file(Path::new("/test/package.json"), content);
89+
90+
// Create a simple index.js so resolution can proceed
91+
fs.add_file(Path::new("/test/index.js"), "export default 42;");
92+
93+
// Create resolver with VFS
94+
let resolver = ResolverGeneric::new_with_file_system(fs, ResolveOptions::default());
95+
96+
// Attempt to resolve - should fail with JSONError
97+
let result = resolver.resolve(Path::new("/test"), "./index.js");
98+
99+
match result {
100+
Err(ResolveError::Json(json_error)) => {
101+
assert!(
102+
json_error
103+
.message
104+
.to_lowercase()
105+
.contains(&expected_message_contains.to_lowercase()),
106+
"Test case '{name}': Expected error message to contain '{expected_message_contains}', but got: {}",
107+
json_error.message
108+
);
109+
assert!(
110+
json_error.path.ends_with("package.json"),
111+
"Test case '{name}': Expected path to end with 'package.json', but got: {:?}",
112+
json_error.path
113+
);
114+
}
115+
Err(other_error) => {
116+
panic!("Test case '{name}': Expected JSONError but got: {other_error:?}");
117+
}
118+
Ok(resolution) => {
119+
panic!(
120+
"Test case '{name}': Expected error but resolution succeeded: {resolution:?}"
121+
);
122+
}
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)