use std::io;
use std::path::Path;
use anyhow::Result;
use crate::LintMatch;
use crate::lines::Lines;
#[derive(Default, Clone, Debug)]
pub struct Opts {
pub extra_lines: (u8, u8),
#[doc(hidden)]
pub _non_exhaustive: (),
}
pub fn report_terminal(
r#match: &LintMatch,
code: &[u8],
path: &Path,
writer: &mut dyn io::Write,
) -> Result<()> {
report_terminal_opts(r#match, code, path, &Opts::default(), writer)
}
pub fn report_terminal_opts(
r#match: &LintMatch,
code: &[u8],
path: &Path,
opts: &Opts,
writer: &mut dyn io::Write,
) -> Result<()> {
let LintMatch {
lint_name,
message,
range,
} = r#match;
writeln!(writer, "warning: [{lint_name}] {message}")?;
let start_row = range.start_point.row;
let end_row = range.end_point.row;
let start_col = range.start_point.col;
let end_col = range.end_point.col;
writeln!(writer, " --> {}:{start_row}:{start_col}", path.display())?;
let width = (end_row + usize::from(opts.extra_lines.1))
.to_string()
.len();
if range.bytes.is_empty() {
return Ok(())
}
let prefix = format!("{:width$} | ", "");
writeln!(writer, "{prefix}")?;
let () = Lines::new(code, range.bytes.start)
.rev()
.skip(1)
.take(opts.extra_lines.0.into())
.collect::<Vec<&[u8]>>()
.into_iter()
.enumerate()
.rev()
.try_for_each(|(row_sub, line)| {
let row = start_row - row_sub - 1;
writeln!(writer, "{row:width$} | {}", String::from_utf8_lossy(line))
})?;
let mut lines = Lines::new(code, range.bytes.start);
if start_row == end_row {
let lprefix = format!("{start_row:width$} | ");
let line = lines.next().unwrap();
writeln!(writer, "{lprefix}{}", String::from_utf8_lossy(line))?;
writeln!(
writer,
"{prefix}{:indent$}{:^<width$}",
"",
"",
indent = start_col,
width = end_col.saturating_sub(start_col)
)?;
} else {
for (idx, row) in (start_row..=end_row).enumerate() {
let lprefix = format!("{row:width$} | ");
let c = if idx == 0 { "/" } else { "|" };
let line = lines.next().unwrap();
writeln!(writer, "{lprefix} {c} {}", String::from_utf8_lossy(line))?;
}
writeln!(writer, "{prefix} |{:_<width$}^", "", width = end_col)?;
}
let () = lines
.take(opts.extra_lines.1.into())
.enumerate()
.try_for_each(|(row_add, line)| {
let row = end_row + row_add + 1;
writeln!(writer, "{row:width$} | {}", String::from_utf8_lossy(line))
})?;
writeln!(writer, "{prefix}")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
use crate::Point;
use crate::Range;
#[test]
fn empty_range_reporting() {
let code = indoc! { r#"
int main() {}
"# };
let m = LintMatch {
lint_name: "bogus-file-extension".to_string(),
message: "by convention BPF C code should use the file extension '.bpf.c'".to_string(),
range: Range {
bytes: 0..0,
start_point: Point::default(),
end_point: Point::default(),
},
};
let mut report = Vec::new();
let () =
report_terminal(&m, code.as_bytes(), Path::new("./no_bytes.c"), &mut report).unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [bogus-file-extension] by convention BPF C code should use the file extension '.bpf.c'
--> ./no_bytes.c:0:0
"# };
assert_eq!(report, expected);
}
#[test]
fn multi_line_report() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx) {
bpf_probe_read(
event.comm,
TASK_COMM_LEN,
prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 68..140,
start_point: Point { row: 2, col: 4 },
end_point: Point { row: 5, col: 17 },
},
};
let mut report = Vec::new();
let () = report_terminal(&m, code.as_bytes(), Path::new("<stdin>"), &mut report).unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:2:4
|
2 | / bpf_probe_read(
3 | | event.comm,
4 | | TASK_COMM_LEN,
5 | | prev->comm);
| |_________________^
|
"# };
assert_eq!(report, expected);
}
#[test]
fn multi_line_report_line_numbers() {
let code = indoc! { r#"
/* A
* bunch
* of
* filling
*/
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx) {
bpf_probe_read(
event.comm,
TASK_COMM_LEN,
prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 103..175,
start_point: Point { row: 7, col: 4 },
end_point: Point { row: 10, col: 17 },
},
};
let mut report = Vec::new();
let () = report_terminal(&m, code.as_bytes(), Path::new("<stdin>"), &mut report).unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:7:4
|
7 | / bpf_probe_read(
8 | | event.comm,
9 | | TASK_COMM_LEN,
10 | | prev->comm);
| |_________________^
|
"# };
assert_eq!(report, expected);
}
#[test]
fn terminal_reporting() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx)
{
struct task_struct *prev = (struct task_struct *)ctx[1];
struct event event = {0};
bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 160..174,
start_point: Point { row: 6, col: 4 },
end_point: Point { row: 6, col: 18 },
},
};
let mut report = Vec::new();
let () = report_terminal(&m, code.as_bytes(), Path::new("<stdin>"), &mut report).unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:6:4
|
6 | bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
| ^^^^^^^^^^^^^^
|
"# };
assert_eq!(report, expected);
}
#[test]
fn report_top_most_line() {
let code = indoc! { r#"
SEC("kprobe/test")
int handle__test(void)
{
}
"# };
let m = LintMatch {
lint_name: "unstable-attach-point".to_string(),
message: "kprobe/kretprobe/fentry/fexit are unstable".to_string(),
range: Range {
bytes: 4..17,
start_point: Point { row: 0, col: 4 },
end_point: Point { row: 0, col: 17 },
},
};
let mut report = Vec::new();
let () = report_terminal(&m, code.as_bytes(), Path::new("<stdin>"), &mut report).unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [unstable-attach-point] kprobe/kretprobe/fentry/fexit are unstable
--> <stdin>:0:4
|
0 | SEC("kprobe/test")
| ^^^^^^^^^^^^^
|
"# };
assert_eq!(report, expected);
}
#[test]
fn report_terminal_opts_none_context() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx)
{
struct task_struct *prev = (struct task_struct *)ctx[1];
struct event event = {0};
bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 160..174,
start_point: Point { row: 5, col: 4 },
end_point: Point { row: 5, col: 18 },
},
};
let mut report_old = Vec::new();
let mut report_new = Vec::new();
let () =
report_terminal(&m, code.as_bytes(), Path::new("<stdin>"), &mut report_old).unwrap();
let () = report_terminal_opts(
&m,
code.as_bytes(),
Path::new("<stdin>"),
&Opts::default(),
&mut report_new,
)
.unwrap();
assert_eq!(report_old, report_new);
}
#[test]
fn report_terminal_opts_with_context() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx)
{
struct task_struct *prev = (struct task_struct *)ctx[1];
struct event event = {0};
bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 160..174,
start_point: Point { row: 5, col: 4 },
end_point: Point { row: 5, col: 18 },
},
};
let mut report = Vec::new();
let () = report_terminal_opts(
&m,
code.as_bytes(),
Path::new("<stdin>"),
&Opts {
extra_lines: (2, 1),
..Default::default()
},
&mut report,
)
.unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:5:4
|
3 | struct task_struct *prev = (struct task_struct *)ctx[1];
4 | struct event event = {0};
5 | bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
| ^^^^^^^^^^^^^^
6 | return 0;
|
"# };
assert_eq!(report, expected);
}
#[test]
fn report_terminal_opts_multiline_with_context() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx) {
bpf_probe_read(
event.comm,
TASK_COMM_LEN,
prev->comm);
return 0;
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 68..140,
start_point: Point { row: 2, col: 4 },
end_point: Point { row: 5, col: 17 },
},
};
let mut report = Vec::new();
let () = report_terminal_opts(
&m,
code.as_bytes(),
Path::new("<stdin>"),
&Opts {
extra_lines: (1, 1),
..Default::default()
},
&mut report,
)
.unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:2:4
|
1 | int handle__sched_switch(u64 *ctx) {
2 | / bpf_probe_read(
3 | | event.comm,
4 | | TASK_COMM_LEN,
5 | | prev->comm);
| |_________________^
6 | return 0;
|
"# };
assert_eq!(report, expected);
}
#[test]
fn report_terminal_opts_insufficient_context_before() {
let code = indoc! { r#"
SEC("kprobe/test")
int handle__test(void)
{
}
"# };
let m = LintMatch {
lint_name: "unstable-attach-point".to_string(),
message: "kprobe/kretprobe/fentry/fexit are unstable".to_string(),
range: Range {
bytes: 4..17,
start_point: Point { row: 0, col: 4 },
end_point: Point { row: 0, col: 17 },
},
};
let mut report = Vec::new();
let () = report_terminal_opts(
&m,
code.as_bytes(),
Path::new("<stdin>"),
&Opts {
extra_lines: (5, 2),
..Default::default()
},
&mut report,
)
.unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [unstable-attach-point] kprobe/kretprobe/fentry/fexit are unstable
--> <stdin>:0:4
|
0 | SEC("kprobe/test")
| ^^^^^^^^^^^^^
1 | int handle__test(void)
2 | {
|
"# };
assert_eq!(report, expected);
}
#[test]
fn report_terminal_opts_insufficient_context_after() {
let code = indoc! { r#"
SEC("tp_btf/sched_switch")
int handle__sched_switch(u64 *ctx)
{
bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
}
"# };
let m = LintMatch {
lint_name: "probe-read".to_string(),
message: "bpf_probe_read() is deprecated".to_string(),
range: Range {
bytes: 68..82,
start_point: Point { row: 3, col: 4 },
end_point: Point { row: 3, col: 18 },
},
};
let mut report = Vec::new();
let () = report_terminal_opts(
&m,
code.as_bytes(),
Path::new("<stdin>"),
&Opts {
extra_lines: (1, 5),
..Default::default()
},
&mut report,
)
.unwrap();
let report = String::from_utf8(report).unwrap();
let expected = indoc! { r#"
warning: [probe-read] bpf_probe_read() is deprecated
--> <stdin>:3:4
|
2 | {
3 | bpf_probe_read(event.comm, TASK_COMM_LEN, prev->comm);
| ^^^^^^^^^^^^^^
4 | }
|
"# };
assert_eq!(report, expected);
}
}