Skip to content

Commit 412466c

Browse files
feat: add gitlab reporter (#624)
1 parent c4d79e3 commit 412466c

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

crates/squawk/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ arg_enum! {
8080
Tty,
8181
Gcc,
8282
Json,
83+
Gitlab,
8384
}
8485
}
8586

crates/squawk/src/reporter.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use squawk_linter::Linter;
1111
use squawk_linter::Rule;
1212
use squawk_linter::Version;
1313
use squawk_syntax::SourceFile;
14+
use std::hash::DefaultHasher;
15+
use std::hash::Hash;
16+
use std::hash::Hasher;
1417
use std::io;
1518
use std::path::PathBuf;
1619
use std::process::ExitCode;
@@ -359,6 +362,86 @@ pub fn fmt_github_annotations<W: io::Write>(f: &mut W, reports: &[CheckReport])
359362
Ok(())
360363
}
361364

365+
#[derive(Serialize)]
366+
struct GitLabLines {
367+
begin: usize,
368+
end: usize,
369+
}
370+
371+
#[derive(Serialize)]
372+
struct GitLabLocation {
373+
path: String,
374+
lines: GitLabLines,
375+
}
376+
377+
#[derive(Serialize)]
378+
struct GitLabIssue {
379+
description: String,
380+
severity: String,
381+
fingerprint: String,
382+
location: GitLabLocation,
383+
check_name: String,
384+
}
385+
386+
impl From<&ViolationLevel> for String {
387+
fn from(level: &ViolationLevel) -> Self {
388+
match level {
389+
ViolationLevel::Warning => "minor".to_string(),
390+
ViolationLevel::Error => "major".to_string(),
391+
}
392+
}
393+
}
394+
395+
fn make_fingerprint(v: &ReportViolation) -> String {
396+
let key = format!(
397+
"{}:{}-{}:{}:{}:{}",
398+
v.file, v.line, v.line_end, v.rule_name, v.message, v.level
399+
);
400+
let mut hasher = DefaultHasher::new();
401+
key.hash(&mut hasher);
402+
format!("{:x}", hasher.finish())
403+
}
404+
405+
fn to_gitlab_issue(v: &ReportViolation) -> GitLabIssue {
406+
let mut desc = v.message.clone();
407+
if let Some(help) = &v.help {
408+
if !help.trim().is_empty() {
409+
desc.push_str(" Suggestion: ");
410+
desc.push_str(help.trim());
411+
}
412+
}
413+
414+
GitLabIssue {
415+
description: desc,
416+
severity: String::from(&v.level),
417+
fingerprint: make_fingerprint(v),
418+
location: GitLabLocation {
419+
path: v.file.clone(),
420+
lines: GitLabLines {
421+
begin: v.line,
422+
end: if v.line_end >= v.line {
423+
v.line_end
424+
} else {
425+
v.line
426+
},
427+
},
428+
},
429+
check_name: v.rule_name.clone(),
430+
}
431+
}
432+
433+
fn fmt_gitlab<W: io::Write>(f: &mut W, reports: Vec<CheckReport>) -> Result<()> {
434+
let issues: Vec<GitLabIssue> = reports
435+
.into_iter()
436+
.flat_map(|r| r.violations.into_iter())
437+
.map(|v| to_gitlab_issue(&v))
438+
.collect();
439+
440+
let json = serde_json::to_string(&issues)?;
441+
writeln!(f, "{json}")?;
442+
Ok(())
443+
}
444+
362445
#[derive(Debug)]
363446
pub struct CheckReport {
364447
pub filename: String,
@@ -379,6 +462,7 @@ pub fn print_violations<W: io::Write>(
379462
Reporter::Gcc => fmt_gcc(writer, &reports),
380463
Reporter::Json => fmt_json(writer, reports),
381464
Reporter::Tty => fmt_tty(writer, &reports),
465+
Reporter::Gitlab => fmt_gitlab(writer, reports),
382466
}
383467
}
384468

@@ -524,6 +608,30 @@ SELECT 1;
524608
assert_snapshot!(String::from_utf8_lossy(&buff), @r#"[{"file":"main.sql","line":1,"column":29,"level":"Warning","message":"Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required.","help":"Make the field nullable or add a non-VOLATILE DEFAULT","rule_name":"adding-required-field","column_end":62,"line_end":1},{"file":"main.sql","line":1,"column":29,"level":"Warning","message":"Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.","help":null,"rule_name":"prefer-robust-stmts","column_end":62,"line_end":1},{"file":"main.sql","line":1,"column":46,"level":"Warning","message":"Using 32-bit integer fields can result in hitting the max `int` limit.","help":"Use 64-bit integer values instead to prevent hitting this limit.","rule_name":"prefer-bigint-over-int","column_end":53,"line_end":1},{"file":"main.sql","line":2,"column":23,"level":"Warning","message":"Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required.","help":"Make the field nullable or add a non-VOLATILE DEFAULT","rule_name":"adding-required-field","column_end":56,"line_end":2},{"file":"main.sql","line":2,"column":23,"level":"Warning","message":"Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.","help":null,"rule_name":"prefer-robust-stmts","column_end":56,"line_end":2},{"file":"main.sql","line":2,"column":40,"level":"Warning","message":"Using 32-bit integer fields can result in hitting the max `int` limit.","help":"Use 64-bit integer values instead to prevent hitting this limit.","rule_name":"prefer-bigint-over-int","column_end":47,"line_end":2}]"#);
525609
}
526610

611+
#[test]
612+
fn display_violations_gitlab() {
613+
let sql = r#"
614+
ALTER TABLE "core_recipe" ADD COLUMN "foo" integer NOT NULL;
615+
ALTER TABLE "core_foo" ADD COLUMN "bar" integer NOT NULL;
616+
SELECT 1;
617+
"#;
618+
let filename = "main.sql";
619+
let mut buff = Vec::new();
620+
621+
let res = print_violations(
622+
&mut buff,
623+
vec![check_sql(sql, filename, &[], None, false)],
624+
&Reporter::Gitlab,
625+
false,
626+
);
627+
628+
assert!(res.is_ok());
629+
630+
assert_snapshot!(String::from_utf8_lossy(&buff), @r###"
631+
[{"description":"Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required. Suggestion: Make the field nullable or add a non-VOLATILE DEFAULT","severity":"minor","fingerprint":"87fbb54d93cdb8c9","location":{"path":"main.sql","lines":{"begin":1,"end":1}},"check_name":"adding-required-field"},{"description":"Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.","severity":"minor","fingerprint":"21df0ee3817ad84","location":{"path":"main.sql","lines":{"begin":1,"end":1}},"check_name":"prefer-robust-stmts"},{"description":"Using 32-bit integer fields can result in hitting the max `int` limit. Suggestion: Use 64-bit integer values instead to prevent hitting this limit.","severity":"minor","fingerprint":"3d0e81dc13bc8757","location":{"path":"main.sql","lines":{"begin":1,"end":1}},"check_name":"prefer-bigint-over-int"},{"description":"Adding a new column that is `NOT NULL` and has no default value to an existing table effectively makes it required. Suggestion: Make the field nullable or add a non-VOLATILE DEFAULT","severity":"minor","fingerprint":"4bdd655ad8e102ad","location":{"path":"main.sql","lines":{"begin":2,"end":2}},"check_name":"adding-required-field"},{"description":"Missing `IF NOT EXISTS`, the migration can't be rerun if it fails part way through.","severity":"minor","fingerprint":"1b2e8c81e717c442","location":{"path":"main.sql","lines":{"begin":2,"end":2}},"check_name":"prefer-robust-stmts"},{"description":"Using 32-bit integer fields can result in hitting the max `int` limit. Suggestion: Use 64-bit integer values instead to prevent hitting this limit.","severity":"minor","fingerprint":"2bed2a431803b811","location":{"path":"main.sql","lines":{"begin":2,"end":2}},"check_name":"prefer-bigint-over-int"}]
632+
"###);
633+
}
634+
527635
#[test]
528636
fn span_offsets() {
529637
let sql = r#"

0 commit comments

Comments
 (0)