use drep::analysis::findings::Severity;
use drep::languages::definitions::{
CPPCHECK, CREDO, DOTNET_FORMAT, GO_VET, GOFMT, HADOLINT, PHPCS, RUBOCOP, RUFF, SHELLCHECK,
SQLFLUFF, SWIFTLINT, TFLINT,
};
use drep::languages::runner::parse_output;
use drep::languages::spec::{DiagnosticsStream, ToolSpec};
const RUFF_JSON: &str = r#"[
{"code":"F401","filename":"/tmp/real/bad.py",
"fix":{"applicability":"safe","message":"Remove unused import: `os`"},
"location":{"column":8,"row":1},"message":"`os` imported but unused"},
{"code":"F401","filename":"/tmp/real/bad.py",
"fix":{"applicability":"safe","message":"Remove unused import: `sys`"},
"location":{"column":8,"row":2},"message":"`sys` imported but unused"},
{"code":"F841","filename":"/tmp/real/bad.py",
"fix":{"applicability":"unsafe","message":"Remove assignment to unused variable `x`"},
"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used"}
]"#;
const GOFMT_LINES: &str = "unformatted.go\n";
const GO_VET_STDERR: &str =
"main.go:6:14: fmt.Printf format %d has arg \"not an int\" of wrong type string\n";
#[test]
fn ruff_real_output_parses() {
let findings = parse_output(&RUFF, RUFF_JSON, "fallback.py").expect("ruff output parses");
assert_eq!(findings.len(), 3, "one finding per ruff diagnostic");
let first = &findings[0];
assert_eq!(first.kind, "F401");
assert_eq!(first.severity, Severity::Error, "tool findings block");
assert_eq!(first.file_path, "/tmp/real/bad.py");
assert_eq!(first.line, 1);
assert_eq!(first.column, Some(8));
assert_eq!(first.message, "`os` imported but unused");
assert_eq!(
first.suggestion.as_deref(),
Some("Remove unused import: `os`"),
"ruff's fix.message is the suggestion"
);
assert_eq!(findings[2].kind, "F841");
assert_eq!(findings[2].line, 6);
}
#[test]
fn gofmt_real_output_parses() {
let findings = parse_output(&GOFMT, GOFMT_LINES, "fallback.go").expect("gofmt output parses");
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].file_path, "unformatted.go");
assert_eq!(findings[0].line, 1, "a formatting complaint is file-level");
assert_eq!(findings[0].severity, Severity::Error);
let suggestion = findings[0].suggestion.as_deref().unwrap_or_default();
assert!(
suggestion.contains("-w") && suggestion.contains("unformatted.go"),
"suggestion should be runnable, got {suggestion:?}"
);
}
#[test]
fn go_vet_real_output_parses() {
let findings = parse_output(&GO_VET, GO_VET_STDERR, "fallback.go").expect("go vet parses");
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].file_path, "main.go");
assert_eq!(findings[0].line, 6);
assert_eq!(findings[0].column, Some(14));
assert!(
findings[0].message.starts_with("fmt.Printf format %d"),
"message should survive intact, got {:?}",
findings[0].message
);
}
#[test]
fn go_vet_package_header_form_also_parses() {
let with_header = "# example.com/bad\n./main.go:6:14: some diagnostic\n";
let findings = parse_output(&GO_VET, with_header, "fallback.go").expect("parses");
assert_eq!(findings.len(), 1, "the header is skipped, not parsed");
assert_eq!(
findings[0].file_path, "main.go",
"the ./ prefix is stripped"
);
}
#[test]
fn a_clean_run_produces_no_findings() {
assert!(
parse_output(&RUFF, "[]", "x.py")
.expect("empty array parses")
.is_empty()
);
assert!(
parse_output(&GOFMT, "", "x.go")
.expect("empty output parses")
.is_empty()
);
assert!(
parse_output(&GO_VET, "", "x.go")
.expect("empty output parses")
.is_empty()
);
}
const SHELLCHECK_JSON: &str = r#"[{"file":"t.sh","line":2,"endLine":2,"column":1,"endColumn":4,"level":"warning","code":2034,"message":"foo appears unused. Verify use (or export if used externally).","fix":null},
{"file":"t.sh","line":3,"endLine":3,"column":6,"endColumn":10,"level":"info","code":2086,"message":"Double quote to prevent globbing and word splitting.","fix":{"replacements":[]}}]"#;
const RUBOCOP_JSON: &str = r#"{"metadata":{"rubocop_version":"1.90.0"},"files":[{"path":"Sample.rb","offenses":[
{"severity":"warning","message":"Useless assignment to variable - `y`.","cop_name":"Lint/UselessAssignment","location":{"start_line":2,"start_column":3,"line":2,"column":3}}]}],
"summary":{"offense_count":1}}"#;
const PHPCS_JSON: &str = r#"{"totals":{"errors":1},"files":{"\/w\/Sample.php":{"errors":1,"warnings":0,"messages":[
{"message":"Opening brace should be on a new line","source":"Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine","severity":5,"fixable":true,"type":"ERROR","line":2,"column":18}]}}}"#;
const CREDO_JSON: &str = r##"{"issues":[
{"category":"design","check":"Credo.Check.Design.TagTODO","column":null,"column_end":null,"filename":"lib/demo.ex","line_no":2,"message":"Found a TODO tag in a comment: # TODO: fix this","priority":1,"scope":"Demo","trigger":"# TODO: fix this"}]}"##;
const SQLFLUFF_JSON: &str = r#"[{"filepath":"migration.sql","violations":[
{"start_line_no":3,"start_line_pos":1,"code":"CP02","description":"Unquoted identifiers must be consistently lower case.","name":"capitalisation.identifiers","warning":false,"fixes":[]}]}]"#;
const DOTNET_FORMAT_STDOUT: &str = "/tmp/cs/Program.cs(4,9): error WHITESPACE: Fix whitespace formatting. Delete 4 characters. [/tmp/cs/cs.csproj]\n";
const CPPCHECK_SARIF: &str = r#"{"version":"2.1.0","runs":[{"results":[
{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"uri":"sample.c"},"region":{"startColumn":6,"startLine":5,"endColumn":6,"endLine":5}}}],
"message":{"text":"Array 'a[5]' accessed at index 5, which is out of bounds."},"ruleId":"arrayIndexOutOfBounds"}]}]}"#;
const SWIFTLINT_SARIF: &str = r#"{"runs":[{"results":[
{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"uri":"Sources/Sample.swift"},"region":{"startColumn":9,"startLine":4}}}],
"message":{"text":"Variable name 'x' should be between 3 and 40 characters long"},"ruleId":"identifier_name"}]}]}"#;
const TFLINT_SARIF: &str = r#"{"runs":[{"results":[
{"ruleId":"terraform_required_providers","ruleIndex":0,"level":"warning","message":{"text":"Missing version constraint for provider \"aws\" in `required_providers`"},
"locations":[{"physicalLocation":{"artifactLocation":{"uri":"main.tf"},"region":{"startLine":1,"startColumn":1,"endLine":1,"endColumn":30}}}]}]}]}"#;
const HADOLINT_SARIF: &str = r#"{"runs":[{"results":[
{"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"Dockerfile"},"region":{"startColumn":1,"startLine":1,"sourceLanguage":"dockerfile"}}}],
"message":{"text":"Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag"},"ruleId":"DL3007"}]}]}"#;
struct RealOutputCase {
spec: &'static ToolSpec,
output: &'static str,
findings: usize,
file: &'static str,
line: u32,
column: Option<u32>,
kind: &'static str,
severity: Severity,
}
#[test]
fn every_registered_tool_parses_its_real_output() {
let cases: &[RealOutputCase] = &[
RealOutputCase {
spec: &SHELLCHECK,
output: SHELLCHECK_JSON,
findings: 2,
file: "t.sh",
line: 2,
column: Some(1),
kind: "SC2034",
severity: Severity::Warning,
},
RealOutputCase {
spec: &RUBOCOP,
output: RUBOCOP_JSON,
findings: 1,
file: "Sample.rb",
line: 2,
column: Some(3),
kind: "Lint/UselessAssignment",
severity: Severity::Warning,
},
RealOutputCase {
spec: &PHPCS,
output: PHPCS_JSON,
findings: 1,
file: "/w/Sample.php",
line: 2,
column: Some(18),
kind: "Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine",
severity: Severity::Error,
},
RealOutputCase {
spec: &CREDO,
output: CREDO_JSON,
findings: 1,
file: "lib/demo.ex",
line: 2,
column: None,
kind: "Credo.Check.Design.TagTODO",
severity: Severity::Warning,
},
RealOutputCase {
spec: &SQLFLUFF,
output: SQLFLUFF_JSON,
findings: 1,
file: "migration.sql",
line: 3,
column: Some(1),
kind: "CP02",
severity: Severity::Error,
},
RealOutputCase {
spec: &DOTNET_FORMAT,
output: DOTNET_FORMAT_STDOUT,
findings: 1,
file: "/tmp/cs/Program.cs",
line: 4,
column: Some(9),
kind: "WHITESPACE",
severity: Severity::Error,
},
RealOutputCase {
spec: &CPPCHECK,
output: CPPCHECK_SARIF,
findings: 1,
file: "sample.c",
line: 5,
column: Some(6),
kind: "arrayIndexOutOfBounds",
severity: Severity::Error,
},
RealOutputCase {
spec: &SWIFTLINT,
output: SWIFTLINT_SARIF,
findings: 1,
file: "Sources/Sample.swift",
line: 4,
column: Some(9),
kind: "identifier_name",
severity: Severity::Error,
},
RealOutputCase {
spec: &TFLINT,
output: TFLINT_SARIF,
findings: 1,
file: "main.tf",
line: 1,
column: Some(1),
kind: "terraform_required_providers",
severity: Severity::Warning,
},
RealOutputCase {
spec: &HADOLINT,
output: HADOLINT_SARIF,
findings: 1,
file: "Dockerfile",
line: 1,
column: Some(1),
kind: "DL3007",
severity: Severity::Warning,
},
];
for case in cases {
let name = case.spec.name;
let findings = parse_output(case.spec, case.output, "fallback")
.unwrap_or_else(|err| panic!("{name} should parse its own output: {err}"));
assert_eq!(
findings.len(),
case.findings,
"{name} must read every finding in its document, not just the first"
);
let first = findings
.first()
.unwrap_or_else(|| panic!("{name} produced no finding from real output"));
assert_eq!(first.file_path, case.file, "{name} file path");
assert_eq!(first.line, case.line, "{name} line");
assert_eq!(first.column, case.column, "{name} column");
assert_eq!(first.kind, case.kind, "{name} rule id");
assert_eq!(first.severity, case.severity, "{name} severity");
}
}
#[test]
fn every_registered_tool_reads_its_own_clean_run() {
let cases: &[(&ToolSpec, &str)] = &[
(&SHELLCHECK, "[]"),
(&RUBOCOP, r#"{"files":[]}"#),
(&PHPCS, r#"{"totals":{"errors":0},"files":{}}"#),
(&CREDO, r#"{"issues":[]}"#),
(&SQLFLUFF, "[]"),
(&DOTNET_FORMAT, ""),
(&CPPCHECK, r#"{"version":"2.1.0","runs":[{"results":[]}]}"#),
(&SWIFTLINT, r#"{"runs":[{"results":[]}]}"#),
(&TFLINT, r#"{"runs":[{"results":[]}]}"#),
(&HADOLINT, r#"{"runs":[{"results":[]}]}"#),
];
for (spec, output) in cases {
let findings = parse_output(spec, output, "fallback")
.unwrap_or_else(|err| panic!("{} clean run should parse: {err}", spec.name));
assert!(
findings.is_empty(),
"{} reported a finding on a clean run",
spec.name
);
}
}
#[test]
fn only_the_verified_tools_read_stderr() {
let mut stderr_tools: Vec<&str> = drep::languages::all_languages()
.iter()
.flat_map(|lang| lang.tools.iter())
.filter(|spec| spec.diagnostics_stream == DiagnosticsStream::Stderr)
.map(|spec| spec.name)
.collect();
stderr_tools.sort_unstable();
assert_eq!(
stderr_tools,
vec!["cppcheck", "cppcheck", "go vet"],
"cppcheck is listed twice because C and C++ share the one spec"
);
}