-
-
Notifications
You must be signed in to change notification settings - Fork 502
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): Junit reporter (#8756)
closes #7960 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
- Loading branch information
1 parent
831928d
commit 7e8568b
Showing
5 changed files
with
213 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
use oxc_diagnostics::{ | ||
reporter::{DiagnosticReporter, DiagnosticResult, Info}, | ||
Error, Severity, | ||
}; | ||
use rustc_hash::FxHashMap; | ||
|
||
use super::{xml_utils::xml_escape, InternalFormatter}; | ||
|
||
#[derive(Default)] | ||
pub struct JUnitOutputFormatter; | ||
|
||
impl InternalFormatter for JUnitOutputFormatter { | ||
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> { | ||
Box::new(JUnitReporter::default()) | ||
} | ||
} | ||
|
||
#[derive(Default)] | ||
struct JUnitReporter { | ||
diagnostics: Vec<Error>, | ||
} | ||
|
||
impl DiagnosticReporter for JUnitReporter { | ||
fn finish(&mut self, _: &DiagnosticResult) -> Option<String> { | ||
Some(format_junit(&self.diagnostics)) | ||
} | ||
|
||
fn render_error(&mut self, error: Error) -> Option<String> { | ||
self.diagnostics.push(error); | ||
None | ||
} | ||
} | ||
|
||
fn format_junit(diagnostics: &[Error]) -> String { | ||
let mut grouped: FxHashMap<String, Vec<&Error>> = FxHashMap::default(); | ||
let mut total_errors = 0; | ||
let mut total_warnings = 0; | ||
|
||
for diagnostic in diagnostics { | ||
let info = Info::new(diagnostic); | ||
grouped.entry(info.filename).or_default().push(diagnostic); | ||
} | ||
|
||
let mut test_suite = String::new(); | ||
for diagnostics in grouped.values() { | ||
let diagnostic = diagnostics[0]; | ||
let filename = Info::new(diagnostic).filename; | ||
let mut test_cases = String::new(); | ||
let mut error = 0; | ||
let mut warning = 0; | ||
|
||
for diagnostic in diagnostics { | ||
let rule = diagnostic.code().map_or_else(String::new, |code| code.to_string()); | ||
let Info { message, start, .. } = Info::new(diagnostic); | ||
|
||
let severity = if let Some(Severity::Error) = diagnostic.severity() { | ||
total_errors += 1; | ||
error += 1; | ||
"error" | ||
} else { | ||
total_warnings += 1; | ||
warning += 1; | ||
"failure" | ||
}; | ||
let description = | ||
format!("line {}, column {}, {}", start.line, start.column, xml_escape(&message)); | ||
|
||
let status = format!( | ||
" <{} message=\"{}\">{}</{}>", | ||
severity, | ||
xml_escape(&message), | ||
description, | ||
severity | ||
); | ||
let test_case = | ||
format!("\n <testcase name=\"{rule}\">\n{status}\n </testcase>"); | ||
test_cases = format!("{test_cases}{test_case}"); | ||
} | ||
test_suite = format!(" <testsuite name=\"{}\" tests=\"{}\" disabled=\"0\" errors=\"{}\" failures=\"{}\">{}\n </testsuite>", filename, diagnostics.len(), error, warning, test_cases); | ||
} | ||
let test_suites = format!("<testsuites name=\"Oxlint\" tests=\"{}\" failures=\"{}\" errors=\"{}\">\n{}\n</testsuites>\n", total_errors + total_warnings, total_warnings, total_errors, test_suite); | ||
|
||
format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{test_suites}") | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use oxc_diagnostics::{reporter::DiagnosticResult, NamedSource, OxcDiagnostic}; | ||
use oxc_span::Span; | ||
|
||
#[test] | ||
fn test_junit_reporter() { | ||
const EXPECTED_REPORT: &str = r#"<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites name="Oxlint" tests="2" failures="1" errors="1"> | ||
<testsuite name="file.js" tests="2" disabled="0" errors="1" failures="1"> | ||
<testcase name=""> | ||
<error message="error message">line 1, column 1, error message</error> | ||
</testcase> | ||
<testcase name=""> | ||
<failure message="warning message">line 1, column 1, warning message</failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites> | ||
"#; | ||
let mut reporter = JUnitReporter::default(); | ||
|
||
let error = OxcDiagnostic::error("error message") | ||
.with_label(Span::new(0, 8)) | ||
.with_source_code(NamedSource::new("file.js", "let a = ;")); | ||
|
||
let warning = OxcDiagnostic::warn("warning message") | ||
.with_label(Span::new(0, 9)) | ||
.with_source_code(NamedSource::new("file.js", "debugger;")); | ||
|
||
reporter.render_error(error); | ||
reporter.render_error(warning); | ||
|
||
let output = reporter.finish(&DiagnosticResult::default()).unwrap(); | ||
assert_eq!(output.to_string(), EXPECTED_REPORT); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
use std::borrow::Cow; | ||
|
||
/// <https://github.com/tafia/quick-xml/blob/6e34a730853fe295d68dc28460153f08a5a12955/src/escapei.rs#L84-L86> | ||
pub fn xml_escape(raw: &str) -> Cow<str> { | ||
xml_escape_impl(raw, |ch| matches!(ch, b'<' | b'>' | b'&' | b'\'' | b'\"')) | ||
} | ||
|
||
fn xml_escape_impl<F: Fn(u8) -> bool>(raw: &str, escape_chars: F) -> Cow<str> { | ||
let bytes = raw.as_bytes(); | ||
let mut escaped = None; | ||
let mut iter = bytes.iter(); | ||
let mut pos = 0; | ||
while let Some(i) = iter.position(|&b| escape_chars(b)) { | ||
if escaped.is_none() { | ||
escaped = Some(Vec::with_capacity(raw.len())); | ||
} | ||
let escaped = escaped.as_mut().expect("initialized"); | ||
let new_pos = pos + i; | ||
escaped.extend_from_slice(&bytes[pos..new_pos]); | ||
match bytes[new_pos] { | ||
b'<' => escaped.extend_from_slice(b"<"), | ||
b'>' => escaped.extend_from_slice(b">"), | ||
b'\'' => escaped.extend_from_slice(b"'"), | ||
b'&' => escaped.extend_from_slice(b"&"), | ||
b'"' => escaped.extend_from_slice(b"""), | ||
|
||
// This set of escapes handles characters that should be escaped | ||
// in elements of xs:lists, because those characters works as | ||
// delimiters of list elements | ||
b'\t' => escaped.extend_from_slice(b"	"), | ||
b'\n' => escaped.extend_from_slice(b" "), | ||
b'\r' => escaped.extend_from_slice(b" "), | ||
b' ' => escaped.extend_from_slice(b" "), | ||
_ => unreachable!( | ||
"Only '<', '>','\', '&', '\"', '\\t', '\\r', '\\n', and ' ' are escaped" | ||
), | ||
} | ||
pos = new_pos + 1; | ||
} | ||
|
||
if let Some(mut escaped) = escaped { | ||
if let Some(raw) = bytes.get(pos..) { | ||
escaped.extend_from_slice(raw); | ||
} | ||
|
||
// SAFETY: we operate on UTF-8 input and search for an one byte chars only, | ||
// so all slices that was put to the `escaped` is a valid UTF-8 encoded strings | ||
Cow::Owned(unsafe { String::from_utf8_unchecked(escaped) }) | ||
} else { | ||
Cow::Borrowed(raw) | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
...nt/src/snapshots/fixtures__output_formatter_diagnostic_--format=junit [email protected]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--- | ||
source: apps/oxlint/src/tester.rs | ||
assertion_line: 95 | ||
--- | ||
########## | ||
arguments: --format=junit test.js | ||
working directory: fixtures/output_formatter_diagnostic | ||
---------- | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<testsuites name="Oxlint" tests="3" failures="2" errors="1"> | ||
<testsuite name="test.js" tests="3" disabled="0" errors="1" failures="2"> | ||
<testcase name="eslint(no-debugger)"> | ||
<error message="`debugger` statement is not allowed">line 5, column 1, `debugger` statement is not allowed</error> | ||
</testcase> | ||
<testcase name="eslint(no-unused-vars)"> | ||
<failure message="Function 'foo' is declared but never used.">line 1, column 10, Function 'foo' is declared but never used.</failure> | ||
</testcase> | ||
<testcase name="eslint(no-unused-vars)"> | ||
<failure message="Parameter 'b' is declared but never used. Unused parameters should start with a '_'.">line 1, column 17, Parameter 'b' is declared but never used. Unused parameters should start with a '_'.</failure> | ||
</testcase> | ||
</testsuite> | ||
</testsuites> | ||
---------- | ||
CLI result: LintFoundErrors | ||
---------- |