Skip to content

Commit 5286d2e

Browse files
haasonsaasclaude
andcommitted
fix: deep-review improvements — zero-lines hunk overflow, regex caching, pre-commit UX
- smart_review_prompt: replace saturating_sub(1) with explicit zero-check to prevent usize::MAX on deletion-only hunks (same class of bug fixed in context_helpers.rs) - interactive: cache compiled regexes at add_ignore_pattern time instead of recompiling on every should_ignore call - pre-commit: add SKIP_TESTS=1 escape hatch for faster iteration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 486ac7a commit 5286d2e

File tree

3 files changed

+82
-32
lines changed

3 files changed

+82
-32
lines changed

.githooks/pre-commit

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@
22
set -e
33

44
echo "==> Running cargo fmt --check..."
5-
cargo fmt --check
6-
if [ $? -ne 0 ]; then
5+
cargo fmt --check || {
76
echo "ERROR: cargo fmt found formatting issues. Run 'cargo fmt' to fix."
87
exit 1
9-
fi
8+
}
109

1110
echo "==> Running cargo clippy..."
12-
cargo clippy --all-targets -- -D warnings
13-
if [ $? -ne 0 ]; then
11+
cargo clippy --all-targets -- -D warnings || {
1412
echo "ERROR: cargo clippy found warnings. Fix them before committing."
1513
exit 1
16-
fi
14+
}
1715

18-
echo "==> Running cargo test..."
19-
cargo test
20-
if [ $? -ne 0 ]; then
21-
echo "ERROR: Tests failed. Fix them before committing."
22-
exit 1
16+
# Tests are slower — run by default but skip with SKIP_TESTS=1
17+
if [ "${SKIP_TESTS:-0}" = "1" ]; then
18+
echo "==> Skipping tests (SKIP_TESTS=1). CI will catch failures."
19+
else
20+
echo "==> Running cargo test..."
21+
cargo test || {
22+
echo "ERROR: Tests failed. Fix them before committing."
23+
echo "Tip: SKIP_TESTS=1 git commit ... to skip tests locally (CI will still run them)."
24+
exit 1
25+
}
2326
fi
2427

2528
echo "==> All pre-commit checks passed."

src/core/interactive.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use crate::adapters::llm::{LLMAdapter, LLMRequest};
22
use anyhow::Result;
33
use regex::Regex;
4-
use std::collections::HashSet;
54

65
pub struct InteractiveCommand {
76
pub command: CommandType,
@@ -250,35 +249,38 @@ Interactive commands respect these configurations."#
250249
/// Will be wired into the review pipeline's triage filter.
251250
#[allow(dead_code)]
252251
pub struct InteractiveProcessor {
253-
ignored_patterns: HashSet<String>,
252+
/// Raw patterns for substring matching (no wildcards).
253+
literal_patterns: Vec<String>,
254+
/// Compiled regexes for glob patterns (contain `*`).
255+
glob_regexes: Vec<Regex>,
254256
}
255257

256258
#[allow(dead_code)]
257259
impl InteractiveProcessor {
258260
pub fn new() -> Self {
259261
Self {
260-
ignored_patterns: HashSet::new(),
262+
literal_patterns: Vec::new(),
263+
glob_regexes: Vec::new(),
261264
}
262265
}
263266

264267
pub fn add_ignore_pattern(&mut self, pattern: &str) {
265-
self.ignored_patterns.insert(pattern.to_string());
268+
if pattern.contains('*') {
269+
let escaped = regex::escape(pattern).replace(r"\*", ".*");
270+
let regex_pattern = format!("^{}$", escaped);
271+
if let Ok(re) = Regex::new(&regex_pattern) {
272+
self.glob_regexes.push(re);
273+
}
274+
} else {
275+
self.literal_patterns.push(pattern.to_string());
276+
}
266277
}
267278

268279
pub fn should_ignore(&self, path: &str) -> bool {
269-
self.ignored_patterns.iter().any(|pattern| {
270-
// Simple glob matching
271-
if pattern.contains('*') {
272-
// Escape regex metacharacters, then convert glob * to .*
273-
let escaped = regex::escape(pattern).replace(r"\*", ".*");
274-
let regex_pattern = format!("^{}$", escaped);
275-
regex::Regex::new(&regex_pattern)
276-
.map(|re| re.is_match(path))
277-
.unwrap_or(false)
278-
} else {
279-
path.contains(pattern)
280-
}
281-
})
280+
self.literal_patterns
281+
.iter()
282+
.any(|p| path.contains(p.as_str()))
283+
|| self.glob_regexes.iter().any(|re| re.is_match(path))
282284
}
283285
}
284286

src/core/smart_review_prompt.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,19 @@ TAGS: [comma-separated relevant tags]
135135

136136
// Format the diff with line numbers and change indicators
137137
for hunk in &diff.hunks {
138+
let new_end = if hunk.new_lines == 0 {
139+
hunk.new_start
140+
} else {
141+
hunk.new_start + hunk.new_lines - 1
142+
};
143+
let old_end = if hunk.old_lines == 0 {
144+
hunk.old_start
145+
} else {
146+
hunk.old_start + hunk.old_lines - 1
147+
};
138148
let hunk_header = format!(
139149
"### Hunk: Lines {}-{} (was {}-{})\n\n",
140-
hunk.new_start,
141-
hunk.new_start + hunk.new_lines.saturating_sub(1),
142-
hunk.old_start,
143-
hunk.old_start + hunk.old_lines.saturating_sub(1)
150+
hunk.new_start, new_end, hunk.old_start, old_end
144151
);
145152
if max_diff_chars > 0 && diff_chars.saturating_add(hunk_header.len()) > max_diff_chars {
146153
diff_truncated = true;
@@ -262,4 +269,42 @@ mod tests {
262269
"Old range should also be inclusive: 5 + 3 - 1 = 7"
263270
);
264271
}
272+
273+
#[test]
274+
fn test_hunk_header_zero_lines_no_overflow() {
275+
// Deletion-only hunk: new_lines=0 must not produce usize::MAX via
276+
// saturating_sub(1) on 0.
277+
let diff = UnifiedDiff {
278+
file_path: PathBuf::from("test.rs"),
279+
old_content: None,
280+
new_content: None,
281+
is_new: false,
282+
is_deleted: false,
283+
is_binary: false,
284+
hunks: vec![DiffHunk {
285+
old_start: 10,
286+
old_lines: 3,
287+
new_start: 10,
288+
new_lines: 0, // deletion-only
289+
context: String::new(),
290+
changes: vec![DiffLine {
291+
content: "removed".to_string(),
292+
change_type: ChangeType::Removed,
293+
old_line_no: Some(10),
294+
new_line_no: None,
295+
}],
296+
}],
297+
};
298+
let (_system, user) =
299+
SmartReviewPromptBuilder::build_enhanced_review_prompt(&diff, &[], 1000, 10000, None)
300+
.unwrap();
301+
// Should say "Lines 10-10", NOT "Lines 10-18446744073709551615"
302+
assert!(
303+
user.contains("Lines 10-10"),
304+
"Zero new_lines should produce start-start range; got: {}",
305+
user.lines()
306+
.find(|l| l.contains("Hunk:"))
307+
.unwrap_or("(no hunk header found)")
308+
);
309+
}
265310
}

0 commit comments

Comments
 (0)