Skip to content

Commit 2b0e19e

Browse files
author
zhaojing.jz
committed
Improve JSON parsing resilience and error messages
- Add strip_trailing_commas() function to handle trailing commas in settings.json - Provide detailed error messages with file path and common JSON syntax issues - Add comprehensive tests for trailing comma stripping - Fixes "trailing comma" parse errors that were causing panics This makes cc-switch more robust when reading hand-edited JSON files. 🤖 Generated with [Qoder][https://qoder.com]
1 parent c145127 commit 2b0e19e

1 file changed

Lines changed: 131 additions & 2 deletions

File tree

src/claude_settings.rs

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@ use std::fs;
55
use crate::config::types::{ClaudeSettings, Configuration, StorageMode};
66
use crate::utils::get_claude_settings_path;
77

8+
/// Remove trailing commas from JSON content to make it more lenient
9+
///
10+
/// Handles trailing commas before `}` and `]` characters, which are common
11+
/// in hand-edited JSON files but not valid in standard JSON.
12+
fn strip_trailing_commas(json: &str) -> String {
13+
// Simple approach: remove commas that appear before closing braces/brackets
14+
// This handles the most common case of trailing commas
15+
let mut result = String::with_capacity(json.len());
16+
let chars: Vec<char> = json.chars().collect();
17+
let mut i = 0;
18+
19+
while i < chars.len() {
20+
let c = chars[i];
21+
22+
// Check if this is a comma followed by optional whitespace and then } or ]
23+
if c == ',' {
24+
// Look ahead to see if the next non-whitespace char is } or ]
25+
let mut j = i + 1;
26+
while j < chars.len() && chars[j].is_whitespace() {
27+
j += 1;
28+
}
29+
30+
if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
31+
// Skip this trailing comma
32+
i += 1;
33+
continue;
34+
}
35+
}
36+
37+
result.push(c);
38+
i += 1;
39+
}
40+
41+
result
42+
}
43+
844
impl ClaudeSettings {
945
/// Load Claude settings from disk
1046
///
@@ -34,8 +70,28 @@ impl ClaudeSettings {
3470
let mut settings: ClaudeSettings = if content.trim().is_empty() {
3571
ClaudeSettings::default()
3672
} else {
37-
serde_json::from_str(&content)
38-
.with_context(|| "Failed to parse Claude settings JSON")?
73+
// Strip trailing commas to handle lenient JSON
74+
let cleaned_content = strip_trailing_commas(&content);
75+
76+
// Try to parse the cleaned content first
77+
match serde_json::from_str(&cleaned_content) {
78+
Ok(s) => s,
79+
Err(e) => {
80+
// Provide helpful error message with the actual parse error
81+
let error_msg = format!(
82+
"Failed to parse Claude settings JSON at {}:\n {}\n\n\
83+
This usually means the JSON file has invalid syntax.\n\
84+
Common issues:\n\
85+
- Trailing commas (e.g., {{\"key\": \"value\",}})\n\
86+
- Missing quotes around keys or values\n\
87+
- Unescaped special characters in strings\n\n\
88+
Please fix the JSON syntax in the file.",
89+
path.display(),
90+
e
91+
);
92+
return Err(anyhow::anyhow!("{}", error_msg));
93+
}
94+
}
3995
};
4096

4197
// Ensure env field exists (handle case where it might be missing from JSON)
@@ -320,3 +376,76 @@ impl ClaudeSettings {
320376
Ok(())
321377
}
322378
}
379+
380+
#[cfg(test)]
381+
mod tests {
382+
use super::*;
383+
384+
#[test]
385+
fn test_strip_trailing_commas_simple() {
386+
let input = r#"{"a": 1,}"#;
387+
let expected = r#"{"a": 1}"#;
388+
assert_eq!(strip_trailing_commas(input), expected);
389+
}
390+
391+
#[test]
392+
fn test_strip_trailing_commas_nested_object() {
393+
let input = r#"{"env": {"KEY": "value",},}"#;
394+
let expected = r#"{"env": {"KEY": "value"}}"#;
395+
assert_eq!(strip_trailing_commas(input), expected);
396+
}
397+
398+
#[test]
399+
fn test_strip_trailing_commas_array() {
400+
let input = r#"{"items": [1, 2, 3,],}"#;
401+
let expected = r#"{"items": [1, 2, 3]}"#;
402+
assert_eq!(strip_trailing_commas(input), expected);
403+
}
404+
405+
#[test]
406+
fn test_strip_trailing_commas_multiline() {
407+
let input = r#"{
408+
"env": {
409+
"KEY": "value",
410+
},
411+
}"#;
412+
let expected = r#"{
413+
"env": {
414+
"KEY": "value"
415+
}
416+
}"#;
417+
assert_eq!(strip_trailing_commas(input), expected);
418+
}
419+
420+
#[test]
421+
fn test_strip_trailing_commas_no_trailing() {
422+
let input = r#"{"a": 1, "b": 2}"#;
423+
assert_eq!(strip_trailing_commas(input), input);
424+
}
425+
426+
#[test]
427+
fn test_strip_trailing_commas_complex() {
428+
let input = r#"{
429+
"env": {
430+
"ANTHROPIC_AUTH_TOKEN": "token",
431+
"ANTHROPIC_BASE_URL": "https://api.example.com",
432+
},
433+
"model": "claude-3-opus",
434+
}"#;
435+
let expected = r#"{
436+
"env": {
437+
"ANTHROPIC_AUTH_TOKEN": "token",
438+
"ANTHROPIC_BASE_URL": "https://api.example.com"
439+
},
440+
"model": "claude-3-opus"
441+
}"#;
442+
assert_eq!(strip_trailing_commas(input), expected);
443+
}
444+
445+
#[test]
446+
fn test_strip_trailing_commas_preserves_inner_commas() {
447+
let input = r#"{"a": 1, "b": 2, "c": 3,}"#;
448+
let expected = r#"{"a": 1, "b": 2, "c": 3}"#;
449+
assert_eq!(strip_trailing_commas(input), expected);
450+
}
451+
}

0 commit comments

Comments
 (0)