Skip to main content

padlock_source/frontends/
suppress.rs

1// padlock-source/src/frontends/suppress.rs
2//
3// Shared helpers for parsing per-finding suppression annotations from source.
4//
5// All supported languages accept a comment immediately before the struct/type
6// declaration:
7//
8//   // padlock: ignore[ReorderSuggestion]
9//   // padlock: ignore[PaddingWaste, FalseSharing]
10//
11// The comment can appear on the line(s) directly preceding the declaration;
12// blank lines between the comment and the struct are skipped.
13//
14// Valid finding kind names:
15//   PaddingWaste, ReorderSuggestion, FalseSharing, LocalityIssue
16
17/// Parse `padlock: ignore[Kind1, Kind2]` from a single comment line.
18/// The input may be a whole line (including leading `//` or `/*`) or
19/// just the content of a doc attribute string (Rust `#[doc = "..."]`).
20///
21/// Returns the list of finding kind names, or an empty vec if the line does
22/// not contain a suppression directive.
23pub fn extract_suppressed_kinds(line: &str) -> Vec<String> {
24    // Strip leading comment markers and whitespace
25    let body = line
26        .trim()
27        .trim_start_matches("///")
28        .trim_start_matches("//")
29        .trim_start_matches("/*")
30        .trim_start_matches('*')
31        .trim();
32
33    // Accept both `padlock:ignore[...]` and `padlock: ignore[...]`
34    let rest = if let Some(r) = body.strip_prefix("padlock:") {
35        r.trim_start()
36    } else {
37        return Vec::new();
38    };
39
40    let rest = if let Some(r) = rest.strip_prefix("ignore[") {
41        r
42    } else {
43        return Vec::new();
44    };
45
46    if let Some(end) = rest.find(']') {
47        rest[..end]
48            .split(',')
49            .map(|s| s.trim().to_string())
50            .filter(|s| !s.is_empty())
51            .collect()
52    } else {
53        Vec::new()
54    }
55}
56
57/// Scan the source text *before* `node_start_byte` for suppression directives.
58///
59/// Walks backward through lines immediately preceding the struct declaration
60/// (skipping blank lines), stops as soon as a non-comment, non-blank line is seen.
61/// Returns all suppressed finding kinds found.
62pub fn suppressed_from_preceding_source(source: &str, node_start_byte: usize) -> Vec<String> {
63    let before = &source[..node_start_byte.min(source.len())];
64    let mut result = Vec::new();
65    let mut found_any_comment = false;
66    for line in before.lines().rev() {
67        let trimmed = line.trim();
68        if trimmed.is_empty() {
69            continue; // blank lines don't break the scan
70        }
71        if trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with('*') {
72            let kinds = extract_suppressed_kinds(trimmed);
73            if !kinds.is_empty() {
74                result.extend(kinds);
75                found_any_comment = true;
76                continue;
77            }
78            // A comment that is NOT a suppression directive stops the scan —
79            // it's a doc comment or other comment, not related to this struct.
80            break;
81        }
82        break; // non-comment, non-blank — stop
83    }
84    let _ = found_any_comment;
85    result
86}
87
88/// Variant for parsers (like the Rust `syn` frontend) that know the 1-indexed
89/// line number of the struct rather than its byte offset.
90///
91/// Scans the comment line(s) immediately *before* `struct_line` (skipping blank
92/// lines) for `// padlock: ignore[...]` directives.
93pub fn suppressed_from_source_line(source: &str, struct_line: u32) -> Vec<String> {
94    if struct_line == 0 {
95        return Vec::new();
96    }
97    // Compute the byte offset of the start of `struct_line` (1-indexed).
98    let mut line_start = 0usize;
99    for (i, line) in source.lines().enumerate() {
100        if i + 1 == struct_line as usize {
101            break;
102        }
103        line_start += line.len() + 1; // +1 for the '\n'
104    }
105    suppressed_from_preceding_source(source, line_start)
106}
107
108// ── tests ─────────────────────────────────────────────────────────────────────
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parses_single_kind() {
116        let kinds = extract_suppressed_kinds("// padlock: ignore[ReorderSuggestion]");
117        assert_eq!(kinds, vec!["ReorderSuggestion"]);
118    }
119
120    #[test]
121    fn parses_multiple_kinds() {
122        let kinds = extract_suppressed_kinds("// padlock: ignore[PaddingWaste, FalseSharing]");
123        assert_eq!(kinds, vec!["PaddingWaste", "FalseSharing"]);
124    }
125
126    #[test]
127    fn parses_no_space_variant() {
128        let kinds = extract_suppressed_kinds("// padlock:ignore[LocalityIssue]");
129        assert_eq!(kinds, vec!["LocalityIssue"]);
130    }
131
132    #[test]
133    fn returns_empty_for_unrelated_comment() {
134        assert!(extract_suppressed_kinds("// some other comment").is_empty());
135        assert!(extract_suppressed_kinds("// padlock:guard=mu").is_empty());
136        assert!(extract_suppressed_kinds("// padlock:ignore").is_empty()); // no brackets
137    }
138
139    #[test]
140    fn parses_doc_comment_style() {
141        let kinds = extract_suppressed_kinds("/// padlock: ignore[FalseSharing]");
142        assert_eq!(kinds, vec!["FalseSharing"]);
143    }
144
145    #[test]
146    fn preceding_source_finds_immediately_before() {
147        let source =
148            "struct Other { int x; };\n// padlock: ignore[ReorderSuggestion]\nstruct Foo {";
149        let byte = source.find("struct Foo").unwrap();
150        let kinds = suppressed_from_preceding_source(source, byte);
151        assert_eq!(kinds, vec!["ReorderSuggestion"]);
152    }
153
154    #[test]
155    fn preceding_source_skips_blank_lines() {
156        let source = "// padlock: ignore[FalseSharing]\n\nstruct Foo {";
157        let byte = source.find("struct Foo").unwrap();
158        let kinds = suppressed_from_preceding_source(source, byte);
159        assert_eq!(kinds, vec!["FalseSharing"]);
160    }
161
162    #[test]
163    fn preceding_source_stops_at_non_suppress_comment() {
164        // A doc comment between the suppress directive and the struct breaks the scan
165        let source = "// padlock: ignore[ReorderSuggestion]\n// Some other doc\nstruct Foo {";
166        let byte = source.find("struct Foo").unwrap();
167        let kinds = suppressed_from_preceding_source(source, byte);
168        // The "Some other doc" comment is between the directive and the struct → scan stops
169        assert!(kinds.is_empty());
170    }
171
172    #[test]
173    fn preceding_source_returns_empty_when_no_directive() {
174        let source = "struct Bar { int x; };\nstruct Foo {";
175        let byte = source.find("struct Foo").unwrap();
176        let kinds = suppressed_from_preceding_source(source, byte);
177        assert!(kinds.is_empty());
178    }
179}