Skip to main content

big_code_analysis/output/
warning_line.rs

1//! Compiler-warning line writers for [`OffenderRecord`] batches.
2//!
3//! Editor-friendly inline warnings: one offender per line, in the
4//! conventional Clang/GCC and MSVC formats that editor quickfix parsers
5//! (VS Code, IntelliJ, Vim) recognize out of the box. CI annotators do
6//! *not* auto-recognize these formats: GitHub Actions surfaces them only
7//! through a registered problem matcher, and other systems (GitLab,
8//! Jenkins warnings-ng) need an equivalent parser configured. This is the
9//! plain compiler-warning line; GitHub Actions' own `::warning file=…::`
10//! workflow-command syntax is produced elsewhere, on the CLI annotation
11//! path (`big-code-analysis-cli/src/check_format.rs`), not by these
12//! writers.
13//!
14//! Clang/GCC ([`write_clang_warning`]):
15//!
16//! ```text
17//! path/to/file.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]
18//! ```
19//!
20//! MSVC ([`write_msvc_warning`]):
21//!
22//! ```text
23//! path\to\file.rs(42,5): warning : cyclomatic 17 exceeds limit 15
24//! ```
25//!
26//! Both writers emit one line per offender. An empty offender slice
27//! produces empty output (zero bytes), not a blank line. Offenders
28//! whose path is not valid UTF-8 are skipped with a warning to stderr.
29
30#![allow(clippy::doc_markdown)]
31
32use std::io::{self, Write};
33
34use crate::output::offenders::{OffenderRecord, TOOL_ID, warn_non_utf8_path};
35
36/// Default column when an offender has no [`OffenderRecord::start_col`].
37/// Both formats require a column; `1` is the conventional placeholder
38/// (matching how Clang itself reports diagnostics whose column is
39/// unknown).
40const DEFAULT_COL: u32 = 1;
41
42/// Write Clang/GCC-style warning lines for `offenders` to `writer`.
43///
44/// Format: `{path}:{line}:{col}: {severity}: {message} [{rule}]`,
45/// terminated by `\n`. Non-UTF-8 paths are skipped with a stderr
46/// warning. An empty `offenders` slice writes nothing.
47///
48/// # Errors
49///
50/// Returns any [`io::Error`] produced by `writer` while emitting a
51/// warning line. Stops at the first error; partially-written output
52/// may have reached the writer before the failure.
53pub fn write_clang_warning<W: Write>(
54    offenders: &[OffenderRecord],
55    mut writer: W,
56) -> io::Result<()> {
57    for record in offenders {
58        let Some(path) = warn_non_utf8_path("clang-warning", &record.path) else {
59            continue;
60        };
61        let line = record.start_line.max(1);
62        let col = record.start_col.unwrap_or(DEFAULT_COL).max(1);
63        writeln!(
64            writer,
65            "{path}:{line}:{col}: {severity}: {message} [{prefix}-{metric}]",
66            severity = record.severity.as_str(),
67            message = record.default_message(),
68            prefix = TOOL_ID,
69            metric = record.metric,
70        )?;
71    }
72    Ok(())
73}
74
75/// Write MSVC-style warning lines for `offenders` to `writer`.
76///
77/// Format: `{path}({line},{col}): {severity} : {message}`, terminated
78/// by `\n`. Note the space before the colon after `severity` — that is
79/// the MSVC convention. On Windows the path uses `\` separators
80/// (matching cl.exe output); on other platforms it is emitted as-is.
81/// Non-UTF-8 paths are skipped with a stderr warning. An empty
82/// `offenders` slice writes nothing.
83///
84/// # Errors
85///
86/// Returns any [`io::Error`] produced by `writer` while emitting a
87/// warning line. Stops at the first error; partially-written output
88/// may have reached the writer before the failure.
89pub fn write_msvc_warning<W: Write>(offenders: &[OffenderRecord], mut writer: W) -> io::Result<()> {
90    for record in offenders {
91        let Some(raw_path) = warn_non_utf8_path("msvc-warning", &record.path) else {
92            continue;
93        };
94        let path = msvc_path(raw_path);
95        let line = record.start_line.max(1);
96        let col = record.start_col.unwrap_or(DEFAULT_COL).max(1);
97        writeln!(
98            writer,
99            "{path}({line},{col}): {severity} : {message}",
100            severity = record.severity.as_str(),
101            message = record.default_message(),
102        )?;
103    }
104    Ok(())
105}
106
107/// On Windows, normalize forward slashes to backslashes so the output
108/// matches the path style cl.exe emits (and quickfix parsers in
109/// Windows IDEs expect). Elsewhere the input is returned untouched —
110/// CI logs running on Linux/macOS keep `/` separators, which the
111/// quickfix parsers also accept.
112#[cfg(windows)]
113fn msvc_path(raw: &str) -> std::borrow::Cow<'_, str> {
114    if raw.contains('/') {
115        std::borrow::Cow::Owned(raw.replace('/', "\\"))
116    } else {
117        std::borrow::Cow::Borrowed(raw)
118    }
119}
120
121#[cfg(not(windows))]
122fn msvc_path(raw: &str) -> &str {
123    raw
124}
125
126#[cfg(test)]
127#[allow(
128    clippy::float_cmp,
129    clippy::cast_precision_loss,
130    clippy::cast_possible_truncation,
131    clippy::cast_sign_loss,
132    clippy::similar_names,
133    clippy::doc_markdown,
134    clippy::needless_raw_string_hashes,
135    clippy::too_many_lines
136)]
137mod tests {
138    use super::*;
139    use crate::output::offenders::Severity;
140    use std::path::PathBuf;
141
142    fn rec(path: &str, metric: &str, value: f64, limit: f64) -> OffenderRecord {
143        OffenderRecord {
144            path: PathBuf::from(path),
145            function: Some("f".into()),
146            start_line: 42,
147            end_line: 50,
148            start_col: Some(5),
149            metric: metric.into(),
150            value,
151            limit,
152            severity: Severity::Warning,
153        }
154    }
155
156    fn render_clang(offenders: &[OffenderRecord]) -> String {
157        let mut buf = Vec::new();
158        write_clang_warning(offenders, &mut buf).expect("writing to Vec is infallible");
159        String::from_utf8(buf).expect("output is UTF-8")
160    }
161
162    fn render_msvc(offenders: &[OffenderRecord]) -> String {
163        let mut buf = Vec::new();
164        write_msvc_warning(offenders, &mut buf).expect("writing to Vec is infallible");
165        String::from_utf8(buf).expect("output is UTF-8")
166    }
167
168    #[test]
169    fn clang_empty_writes_nothing() {
170        assert_eq!(render_clang(&[]), "");
171    }
172
173    #[test]
174    fn msvc_empty_writes_nothing() {
175        assert_eq!(render_msvc(&[]), "");
176    }
177
178    #[test]
179    fn clang_single_offender() {
180        let out = render_clang(&[rec("src/foo.rs", "cyclomatic", 17.0, 15.0)]);
181        assert_eq!(
182            out,
183            "src/foo.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]\n"
184        );
185    }
186
187    #[test]
188    fn msvc_single_offender() {
189        let out = render_msvc(&[rec("src/foo.rs", "cyclomatic", 17.0, 15.0)]);
190        // On non-Windows, separators are preserved as-is.
191        #[cfg(not(windows))]
192        assert_eq!(
193            out,
194            "src/foo.rs(42,5): warning : cyclomatic 17 exceeds limit 15\n"
195        );
196        #[cfg(windows)]
197        assert_eq!(
198            out,
199            "src\\foo.rs(42,5): warning : cyclomatic 17 exceeds limit 15\n"
200        );
201    }
202
203    #[test]
204    fn clang_missing_column_defaults_to_one() {
205        let mut r = rec("a.rs", "cognitive", 30.0, 15.0);
206        r.start_col = None;
207        let out = render_clang(&[r]);
208        assert!(out.starts_with("a.rs:42:1: warning: "), "{out}");
209    }
210
211    #[test]
212    fn msvc_missing_column_defaults_to_one() {
213        let mut r = rec("a.rs", "cognitive", 30.0, 15.0);
214        r.start_col = None;
215        let out = render_msvc(&[r]);
216        assert!(out.starts_with("a.rs(42,1): warning : "), "{out}");
217    }
218
219    #[test]
220    fn clang_error_severity_renders_error_token() {
221        let mut r = rec("a.rs", "cyclomatic", 99.0, 15.0);
222        r.severity = Severity::Error;
223        let out = render_clang(&[r]);
224        assert!(out.contains(": error: "), "{out}");
225    }
226
227    #[test]
228    fn msvc_error_severity_renders_error_token() {
229        let mut r = rec("a.rs", "cyclomatic", 99.0, 15.0);
230        r.severity = Severity::Error;
231        let out = render_msvc(&[r]);
232        // Note the space before the colon: "error :" not "error:".
233        assert!(out.contains("): error : "), "{out}");
234    }
235
236    #[test]
237    fn clang_integer_value_has_no_decimal_point() {
238        let out = render_clang(&[rec("a.rs", "cyclomatic", 17.0, 15.0)]);
239        assert!(out.contains("cyclomatic 17 exceeds limit 15"), "{out}");
240        assert!(!out.contains("17.0"), "{out}");
241        assert!(!out.contains("15.0"), "{out}");
242    }
243
244    #[test]
245    fn clang_fractional_value_renders_decimals() {
246        let out = render_clang(&[rec("a.rs", "halstead.volume", 12.5, 10.0)]);
247        assert!(
248            out.contains("halstead.volume 12.5 exceeds limit 10"),
249            "{out}"
250        );
251    }
252
253    #[test]
254    fn clang_zero_start_line_clamps_to_one() {
255        let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
256        r.start_line = 0;
257        let out = render_clang(&[r]);
258        assert!(out.starts_with("a.rs:1:5: "), "{out}");
259    }
260
261    #[test]
262    fn msvc_zero_start_line_clamps_to_one() {
263        let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
264        r.start_line = 0;
265        let out = render_msvc(&[r]);
266        assert!(out.starts_with("a.rs(1,5): "), "{out}");
267    }
268
269    #[test]
270    fn clang_multi_offender_one_line_each() {
271        let offenders = vec![
272            rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
273            rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
274            rec("src/zeta.rs", "cognitive", 30.0, 15.0),
275        ];
276        let out = render_clang(&offenders);
277        assert_eq!(out.lines().count(), 3);
278        assert!(out.ends_with('\n'));
279    }
280
281    #[test]
282    fn clang_function_name_does_not_appear_in_line() {
283        // The Clang format has no field for a function name; the
284        // writer must not silently smuggle it in.
285        let r = rec("a.rs", "cyclomatic", 17.0, 15.0);
286        // function is Some("f")
287        let out = render_clang(&[r]);
288        // The bare token "f" could appear inside other words; assert
289        // the structural shape instead.
290        assert_eq!(
291            out,
292            "a.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]\n"
293        );
294    }
295
296    #[test]
297    fn clang_empty_snapshot() {
298        insta::assert_snapshot!("clang_warning_empty", render_clang(&[]));
299    }
300
301    #[test]
302    fn clang_multi_snapshot() {
303        let mut err = rec("src/zeta.rs", "cognitive", 30.0, 15.0);
304        err.severity = Severity::Error;
305        err.start_col = None;
306        err.function = None;
307        let offenders = vec![
308            rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
309            rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
310            err,
311        ];
312        insta::assert_snapshot!("clang_warning_multi", render_clang(&offenders));
313    }
314
315    #[test]
316    fn msvc_empty_snapshot() {
317        insta::assert_snapshot!("msvc_warning_empty", render_msvc(&[]));
318    }
319
320    // The committed snapshot pins forward-slash separators. On Windows
321    // `render_msvc` renders backslashes (verified by
322    // `msvc_path_uses_backslashes_on_windows`), so gate the multi-offender
323    // snapshot to non-Windows.
324    #[cfg(not(windows))]
325    #[test]
326    fn msvc_multi_snapshot() {
327        let mut err = rec("src/zeta.rs", "cognitive", 30.0, 15.0);
328        err.severity = Severity::Error;
329        err.start_col = None;
330        err.function = None;
331        let offenders = vec![
332            rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
333            rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
334            err,
335        ];
336        insta::assert_snapshot!("msvc_warning_multi", render_msvc(&offenders));
337    }
338
339    #[cfg(windows)]
340    #[test]
341    fn msvc_path_uses_backslashes_on_windows() {
342        let out = render_msvc(&[rec("src/foo/bar.rs", "cyclomatic", 17.0, 15.0)]);
343        assert!(out.starts_with("src\\foo\\bar.rs("), "{out}");
344    }
345
346    #[cfg(not(windows))]
347    #[test]
348    fn msvc_path_keeps_forward_slashes_off_windows() {
349        let out = render_msvc(&[rec("src/foo/bar.rs", "cyclomatic", 17.0, 15.0)]);
350        assert!(out.starts_with("src/foo/bar.rs("), "{out}");
351    }
352}