Skip to main content

big_code_analysis/vcs/
classify.rs

1//! Commit-message classification: bug-fix, security-fix, and revert
2//! detection via curated keyword regexes.
3//!
4//! The keyword approach follows the commit-message classification
5//! literature (Pascarella/Bavota for bug-fix detection; the
6//! Sentence-Level VFC studies and PySecDB for security fixes). It is a
7//! coarse signal by design — full SZZ bug-inducing-commit detection is
8//! explicitly out of scope for v1 (issue #328) — so the patterns favour
9//! precision (word-boundary anchored, false-positive-aware) over
10//! recall.
11
12use std::sync::LazyLock;
13
14use regex::Regex;
15
16/// What a single commit message matched.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct Classification {
19    /// The message matched a bug-fix keyword.
20    pub bug_fix: bool,
21    /// The message matched a security-fix keyword.
22    pub security_fix: bool,
23    /// The subject is a revert / rollback.
24    pub revert: bool,
25}
26
27// Word boundaries (`\b`) keep "prefix"/"suffix" from matching `fix` and
28// "insecurity" from matching `security`; the regression tests in
29// `classify_tests.rs` pin exactly those false-positive cases.
30//
31// The patterns are compile-time constants, so the `expect` in each
32// `LazyLock` initialiser guards a provably-unreachable failure (a
33// malformed literal would fail the test suite's `patterns_compile`
34// before any release).
35static BUG_FIX: LazyLock<Regex> = LazyLock::new(|| {
36    Regex::new(
37        r"(?i)\b(?:fix(?:es|ed|ing)?|bug(?:fix)?(?:es|s)?|defect|hotfix|regression|fault|crash)\b",
38    )
39    .expect("BUG_FIX pattern is valid")
40});
41
42// `injection` and `overflow` are bare-term false-positive magnets:
43// "dependency injection", "text overflow", and arithmetic "integer
44// overflow" are routine non-security commits (issue #808). The
45// precision-over-recall contract requires a security-specific
46// qualifier, so each is gated behind a `\s+`-joined attack-vector
47// token. "integer overflow" and "stack overflow" are deliberately
48// excluded: the former is an ordinary arithmetic bug far more often
49// than a security finding, and the latter doubles as the website
50// name — both are ambiguous, and precision wins over recall.
51static SECURITY_FIX: LazyLock<Regex> = LazyLock::new(|| {
52    Regex::new(
53        r"(?i)\b(?:security|vulnerabilit(?:y|ies)|vuln|exploit|sanitiz(?:e|ation)|insecure|xss|csrf|rce|disclosure|malicious|hijack|spoof|(?:sql|command|code|html|ldap|os|xml)\s+injection|(?:buffer|heap)\s+overflow)\b|CVE-\d{4}-\d+|CWE-\d+",
54    )
55    .expect("SECURITY_FIX pattern is valid")
56});
57
58static REVERT_SUBJECT: LazyLock<Regex> =
59    LazyLock::new(|| Regex::new(r"(?i)^revert\b").expect("REVERT_SUBJECT pattern is valid"));
60
61// Rollback gets the same subject-line precision discipline as revert
62// (issue #806): a body-prose "rollback" mention must not flip the
63// whole commit. `^`-anchored to mirror REVERT_SUBJECT — a leading
64// "Rollback the migration" still classifies, a buried one does not.
65static ROLLBACK: LazyLock<Regex> =
66    LazyLock::new(|| Regex::new(r"(?i)^rollback\b").expect("ROLLBACK pattern is valid"));
67
68/// Classify a raw commit message.
69///
70/// The message is matched lossily as UTF-8 — classification is a
71/// heuristic over human-readable prose, never an identifier, so a
72/// non-UTF-8 byte degrading to U+FFFD cannot corrupt downstream state.
73#[must_use]
74pub fn classify(message: &[u8]) -> Classification {
75    let text = String::from_utf8_lossy(message);
76    // The subject is the first line; `^Revert ...` is git's
77    // auto-generated revert subject.
78    let subject = text.lines().next().unwrap_or("");
79    Classification {
80        bug_fix: BUG_FIX.is_match(&text),
81        security_fix: SECURITY_FIX.is_match(&text),
82        revert: REVERT_SUBJECT.is_match(subject) || ROLLBACK.is_match(subject),
83    }
84}
85
86#[cfg(test)]
87#[path = "classify_tests.rs"]
88mod tests;