-
Notifications
You must be signed in to change notification settings - Fork 1
feat(work-e55f69ed): add # Errors sections to core public APIs #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eb9f979
f5820a8
22f4224
40d20d9
ccb2ff0
f1fb07b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # ADR-011: Remove Unnecessary Result Wrapper from parse_blame_porcelain | ||
|
|
||
| **Status:** Accepted | ||
|
|
||
| **Date:** 2026-04-11 | ||
|
|
||
| **Work Item:** work-430b0729 | ||
|
|
||
| --- | ||
|
|
||
| ## Context | ||
|
|
||
| Issue #141 reports that `parse_blame_porcelain` in `crates/diffguard/src/main.rs` (line 1768) is typed to return `Result<BTreeMap<u32, BlameLineMeta>>` but never actually returns `Err`. This is dead code — the function silently skips invalid entries via `continue` rather than propagating errors, and always reaches `Ok(out)` at line 1818. | ||
|
|
||
| Clippy detects this pattern with the `unnecessary_result_bool` lint (or equivalent): *"this function's return value is unnecessarily wrapped by `Result`"*. | ||
|
|
||
| --- | ||
|
|
||
| ## Decision | ||
|
|
||
| Change `parse_blame_porcelain` to return `BTreeMap<u32, BlameLineMeta>` directly, removing the `Result` wrapper. | ||
|
|
||
| ### Changes Required | ||
|
|
||
| 1. **Function signature (line 1768):** | ||
| ```rust | ||
| // Before | ||
| fn parse_blame_porcelain(blame_text: &str) -> Result<BTreeMap<u32, BlameLineMeta>> | ||
|
|
||
| // After | ||
| fn parse_blame_porcelain(blame_text: &str) -> BTreeMap<u32, BlameLineMeta> | ||
| ``` | ||
|
|
||
| 2. **Return expression (line 1818):** | ||
| ```rust | ||
| // Before | ||
| Ok(out) | ||
|
|
||
| // After | ||
| out | ||
| ``` | ||
|
|
||
| 3. **Caller in `collect_blame_allowed_lines` (lines 1861-1862):** | ||
| ```rust | ||
| // Before | ||
| let blame_map = parse_blame_porcelain(&blame_text) | ||
| .with_context(|| format!("parse git blame for {}", path))?; | ||
|
|
||
| // After | ||
| let blame_map = parse_blame_porcelain(&blame_text); | ||
| ``` | ||
|
|
||
| 4. **Test at line 4068:** | ||
| ```rust | ||
| // Before | ||
| let map = parse_blame_porcelain(porcelain).expect("parse"); | ||
|
|
||
| // After | ||
| let map = parse_blame_porcelain(porcelain); | ||
| ``` | ||
|
|
||
| ### Rationale for Silent-Skip Behavior | ||
|
|
||
| The parsing logic skips malformed entries rather than failing because: | ||
| - Git blame output for files with unusual content (binary, untrusted encoding) may contain partial/invalid entries | ||
| - The function is used to extract allowed-line metadata for diff checking — incomplete data is tolerable, hard failure is not | ||
| - This behavior is established and users depend on it | ||
|
|
||
| --- | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| ### 1. Keep Result and document the never-err case | ||
| Adding a comment like `// SAFETY: this function never returns Err` would suppress the lint but leave unnecessary complexity for callers. | ||
|
|
||
| ### 2. Return Option instead of bare BTreeMap | ||
| `Option<BTreeMap<u32, BlameLineMeta>>` would allow `None` for parse failures, but no call site checks for `Err` so `None` would be equally unused. The bare type is cleaner. | ||
|
|
||
| ### 3. Make the function return Result and propagate real errors | ||
| Adding proper error propagation would be a breaking change to the call sites' logic and is out of scope for this fix. | ||
|
|
||
| --- | ||
|
|
||
| ## Consequences | ||
|
|
||
| **Positive:** | ||
| - Removes dead error-handling code from callers | ||
| - Eliminates Clippy lint | ||
| - Improves code clarity — readers know the function cannot fail | ||
| - Removes `.expect()` from test, making test failure messages cleaner | ||
|
|
||
| **Negative:** | ||
| - None — this is purely a refactor with no behavioral change | ||
|
|
||
| **Neutral:** | ||
| - The `anyhow::Result` type alias used throughout the crate remains; this only affects one function's return type | ||
|
|
||
| --- | ||
|
|
||
| ## Files Affected | ||
|
|
||
| - `crates/diffguard/src/main.rs` — function definition (line 1768), return (line 1818), caller (lines 1861-1862), test (line 4068) | ||
|
|
||
| --- | ||
|
|
||
| ## Verification | ||
|
|
||
| After applying changes: | ||
| 1. Run `cargo clippy -p diffguard` — confirm no lint warnings related to `parse_blame_porcelain` | ||
| 2. Run `cargo test -p diffguard` — confirm all tests pass, especially `parse_blame_porcelain_extracts_line_metadata` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||||||||||||||
| //! XML utility functions for diffguard output formatters. | ||||||||||||||||||
| //! | ||||||||||||||||||
| //! Provides shared XML escaping functionality used by JUnit, Checkstyle, | ||||||||||||||||||
| //! and other XML-based output formats. | ||||||||||||||||||
|
|
||||||||||||||||||
| /// Escapes special XML characters and illegal control characters in a string. | ||||||||||||||||||
| /// | ||||||||||||||||||
| /// Handles: | ||||||||||||||||||
| /// - 5 named XML entities: `&`, `<`, `>`, `"`, `'` | ||||||||||||||||||
| /// - Illegal control characters (0x00-0x1F except tab/LF/CR) as `&#xNN;` entities | ||||||||||||||||||
| /// | ||||||||||||||||||
| /// Legal control characters (tab=0x09, LF=0x0A, CR=0x0D) are preserved as-is | ||||||||||||||||||
| /// since they are allowed in XML character content. | ||||||||||||||||||
| pub fn escape_xml(s: &str) -> String { | ||||||||||||||||||
| let mut out = String::with_capacity(s.len()); | ||||||||||||||||||
| for c in s.chars() { | ||||||||||||||||||
| match c { | ||||||||||||||||||
| '&' => out.push_str("&"), | ||||||||||||||||||
| '<' => out.push_str("<"), | ||||||||||||||||||
| '>' => out.push_str(">"), | ||||||||||||||||||
| '"' => out.push_str("""), | ||||||||||||||||||
| '\'' => out.push_str("'"), | ||||||||||||||||||
| // Illegal XML control characters (0x00-0x1F except tab/LF/CR) | ||||||||||||||||||
| c if c <= '\u{001F}' && c != '\t' && c != '\n' && c != '\r' => { | ||||||||||||||||||
| out.push_str(&format!("&#x{:X};", c as u32)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: No, in XML 1.0, numeric character references like � (U+0000) or � (U+001B) are not valid for forbidden control characters in the range U+0000-U+001F (except U+0009 tab, U+000A LF, U+000D CR). The XML 1.0 specification defines valid characters in production [2] Char as Citations:
Numeric character references for forbidden XML 1.0 control characters are invalid and will break XML parsers. The code at lines 24–25 encodes forbidden control characters (U+0000–U+001F except tab, LF, CR) as numeric character references like Replace the encoding with dropping the forbidden characters: Proposed fix- c if c <= '\u{001F}' && c != '\t' && c != '\n' && c != '\r' => {
- out.push_str(&format!("&#x{:X};", c as u32));
- }
+ // Forbidden XML 1.0 control characters—drop them
+ '\u{0000}'..='\u{0008}'
+ | '\u{000B}'
+ | '\u{000C}'
+ | '\u{000E}'..='\u{001F}' => {}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| _ => out.push(c), | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| out | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||
| mod tests { | ||||||||||||||||||
| use super::*; | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn escape_xml_handles_all_special_chars() { | ||||||||||||||||||
| assert_eq!(escape_xml("&"), "&"); | ||||||||||||||||||
| assert_eq!(escape_xml("<"), "<"); | ||||||||||||||||||
| assert_eq!(escape_xml(">"), ">"); | ||||||||||||||||||
| assert_eq!(escape_xml("\""), """); | ||||||||||||||||||
| assert_eq!(escape_xml("'"), "'"); | ||||||||||||||||||
| assert_eq!(escape_xml("normal text"), "normal text"); | ||||||||||||||||||
| assert_eq!(escape_xml("<a & b>"), "<a & b>"); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn escape_xml_escapes_illegal_control_chars() { | ||||||||||||||||||
| // NUL | ||||||||||||||||||
| let result = escape_xml("a\x00b"); | ||||||||||||||||||
| assert!(result.contains("�")); | ||||||||||||||||||
| assert!(!result.contains('\x00')); | ||||||||||||||||||
|
|
||||||||||||||||||
| // BEL (0x07) | ||||||||||||||||||
| let result = escape_xml("a\x07b"); | ||||||||||||||||||
| assert!(result.contains("")); | ||||||||||||||||||
|
|
||||||||||||||||||
| // ESC (0x1B) | ||||||||||||||||||
| let result = escape_xml("a\x1Bb"); | ||||||||||||||||||
| assert!(result.contains("")); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn escape_xml_preserves_legal_control_chars() { | ||||||||||||||||||
| // Tab | ||||||||||||||||||
| let result = escape_xml("a\tb"); | ||||||||||||||||||
| assert!(result.contains('\t')); | ||||||||||||||||||
| assert!(!result.contains("	")); | ||||||||||||||||||
|
|
||||||||||||||||||
| // LF | ||||||||||||||||||
| let result = escape_xml("a\nb"); | ||||||||||||||||||
| assert!(result.contains('\n')); | ||||||||||||||||||
| assert!(!result.contains("
")); | ||||||||||||||||||
|
|
||||||||||||||||||
| // CR | ||||||||||||||||||
| let result = escape_xml("a\rb"); | ||||||||||||||||||
| assert!(result.contains('\r')); | ||||||||||||||||||
| assert!(!result.contains("
")); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| #[test] | ||||||||||||||||||
| fn escape_xml_empty_string() { | ||||||||||||||||||
| assert_eq!(escape_xml(""), ""); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What is the exact Clippy lint name for functions that unnecessarily return Result (i.e., can never return Err)?💡 Result:
The exact Clippy lint name is unnecessary_wraps. It detects private functions that only return Ok or Some, meaning they unnecessarily wrap values in Result or Option when no Err or None is ever returned.
Citations:
Resultreturn types which only returnOk(...)orErr(...)rust-lang/rust-clippy#12119unnecessary_wraprust-lang/rust-clippy#6070unnecessary_wraps: do not include the whole body in the lint span rust-lang/rust-clippy#14777Update Clippy lint name in ADR.
The ADR references
unnecessary_result_bool, which is not a Clippy lint. The correct lint name isunnecessary_wraps—it detects private functions that unnecessarily returnResultorOptionwhen they never returnErrorNone. Update line 15 to use the accurate lint name.🤖 Prompt for AI Agents