Skip to content

Commit 10e5920

Browse files
committedJan 24, 2025·
refactor(linter): move finishing default diagnostic message to GraphicalReporter (#8683)
Now every lint output is owned by is right OutputFormatter and his DiagnosticReporter 🥳 Next step is to setup a snapshot Tester, so I can remove the ToDos. Reorded some lines so the outfor is now for: `cargo run -p oxlint -- test.js --max-warnings=2` ``` Found 4 warnings and 0 errors. Exceeded maximum number of warnings. Found 4. Finished in 5ms on 1 file with 97 rules using 24 threads. ``` and for `cargo run -p oxlint -- test.js` ``` Found 4 warnings and 0 errors. Finished in 5ms on 1 file with 97 rules using 24 threads. ``` The output time and warnings/error count wil be always printed.
1 parent b977678 commit 10e5920

File tree

7 files changed

+189
-94
lines changed

7 files changed

+189
-94
lines changed
 

Diff for: ‎apps/oxlint/src/lint.rs

+28-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use std::{
22
env, fs,
3-
io::{BufWriter, Write},
3+
io::{BufWriter, ErrorKind, Write},
44
path::{Path, PathBuf},
5+
process::ExitCode,
56
time::Instant,
67
};
78

@@ -16,7 +17,7 @@ use serde_json::Value;
1617

1718
use crate::{
1819
cli::{CliRunResult, LintCommand, LintResult, MiscOptions, Runner, WarningOptions},
19-
output_formatter::{OutputFormat, OutputFormatter},
20+
output_formatter::{LintCommandInfo, OutputFormatter},
2021
walk::{Extensions, Walk},
2122
};
2223

@@ -55,7 +56,6 @@ impl Runner for LintRunner {
5556
ignore_options,
5657
fix_options,
5758
enable_plugins,
58-
output_options,
5959
misc_options,
6060
..
6161
} = self.options;
@@ -81,11 +81,8 @@ impl Runner for LintRunner {
8181
// If explicit paths were provided, but all have been
8282
// filtered, return early.
8383
if provided_path_count > 0 {
84-
return CliRunResult::LintResult(LintResult {
85-
duration: now.elapsed(),
86-
deny_warnings: warning_options.deny_warnings,
87-
..LintResult::default()
88-
});
84+
// ToDo: when oxc_linter (config) validates the configuration, we can use exit_code = 1 to fail
85+
return CliRunResult::LintResult(LintResult::default());
8986
}
9087

9188
paths.push(self.cwd.clone());
@@ -211,15 +208,24 @@ impl Runner for LintRunner {
211208

212209
let diagnostic_result = diagnostic_service.run(&mut stdout);
213210

214-
CliRunResult::LintResult(LintResult {
215-
duration: now.elapsed(),
211+
let diagnostic_failed = diagnostic_result.max_warnings_exceeded()
212+
|| diagnostic_result.errors_count() > 0
213+
|| (warning_options.deny_warnings && diagnostic_result.warnings_count() > 0);
214+
215+
if let Some(end) = output_formatter.lint_command_info(&LintCommandInfo {
216+
number_of_files,
216217
number_of_rules: lint_service.linter().number_of_rules(),
218+
threads_count: rayon::current_num_threads(),
219+
start_time: now.elapsed(),
220+
}) {
221+
stdout.write_all(end.as_bytes()).or_else(Self::check_for_writer_error).unwrap();
222+
};
223+
224+
CliRunResult::LintResult(LintResult {
217225
number_of_files,
218226
number_of_warnings: diagnostic_result.warnings_count(),
219227
number_of_errors: diagnostic_result.errors_count(),
220-
max_warnings_exceeded: diagnostic_result.max_warnings_exceeded(),
221-
deny_warnings: warning_options.deny_warnings,
222-
print_summary: matches!(output_options.format, OutputFormat::Default),
228+
exit_code: ExitCode::from(u8::from(diagnostic_failed)),
223229
})
224230
}
225231
}
@@ -306,6 +312,15 @@ impl LintRunner {
306312
let config_path = cwd.join(Self::DEFAULT_OXLINTRC);
307313
Oxlintrc::from_file(&config_path).or_else(|_| Ok(Oxlintrc::default()))
308314
}
315+
316+
fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> {
317+
// Do not panic when the process is killed (e.g. piping into `less`).
318+
if matches!(error.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) {
319+
Ok(())
320+
} else {
321+
Err(error)
322+
}
323+
}
309324
}
310325

311326
#[cfg(test)]
@@ -364,7 +379,6 @@ mod test {
364379
fn no_arg() {
365380
let args = &[];
366381
let result = test(args);
367-
assert!(result.number_of_rules > 0);
368382
assert!(result.number_of_warnings > 0);
369383
assert_eq!(result.number_of_errors, 0);
370384
}
@@ -373,7 +387,6 @@ mod test {
373387
fn dir() {
374388
let args = &["fixtures/linter"];
375389
let result = test(args);
376-
assert!(result.number_of_rules > 0);
377390
assert_eq!(result.number_of_files, 3);
378391
assert_eq!(result.number_of_warnings, 3);
379392
assert_eq!(result.number_of_errors, 0);
@@ -383,7 +396,6 @@ mod test {
383396
fn cwd() {
384397
let args = &["debugger.js"];
385398
let result = test_with_cwd("fixtures/linter", args);
386-
assert!(result.number_of_rules > 0);
387399
assert_eq!(result.number_of_files, 1);
388400
assert_eq!(result.number_of_warnings, 1);
389401
assert_eq!(result.number_of_errors, 0);

Diff for: ‎apps/oxlint/src/output_formatter/checkstyle.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn format_checkstyle(diagnostics: &[Error]) -> String {
6666
format!(r#"<file name="{filename}">{messages}</file>"#)
6767
}).collect::<Vec<_>>().join(" ");
6868
format!(
69-
r#"<?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3">{messages}</checkstyle>"#
69+
"<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle version=\"4.3\">{messages}</checkstyle>\n"
7070
)
7171
}
7272

@@ -148,6 +148,6 @@ mod test {
148148
let second_result = reporter.finish(&DiagnosticResult::default());
149149

150150
assert!(second_result.is_some());
151-
assert_eq!(second_result.unwrap(), "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle version=\"4.3\"><file name=\"file://test.ts\"><error line=\"1\" column=\"1\" severity=\"warning\" message=\"error message\" source=\"\" /></file></checkstyle>");
151+
assert_eq!(second_result.unwrap(), "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle version=\"4.3\"><file name=\"file://test.ts\"><error line=\"1\" column=\"1\" severity=\"warning\" message=\"error message\" source=\"\" /></file></checkstyle>\n");
152152
}
153153
}

Diff for: ‎apps/oxlint/src/output_formatter/default.rs

+109-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::io::Write;
1+
use std::{io::Write, time::Duration};
22

33
use oxc_diagnostics::{
44
reporter::{DiagnosticReporter, DiagnosticResult},
@@ -21,11 +21,34 @@ impl InternalFormatter for DefaultOutputFormatter {
2121
writeln!(writer, "Total: {}", table.total).unwrap();
2222
}
2323

24+
fn lint_command_info(&self, lint_command_info: &super::LintCommandInfo) -> Option<String> {
25+
let time = Self::get_execution_time(&lint_command_info.start_time);
26+
let s = if lint_command_info.number_of_files == 1 { "" } else { "s" };
27+
28+
Some(format!(
29+
"Finished in {time} on {} file{s} with {} rules using {} threads.\n",
30+
lint_command_info.number_of_files,
31+
lint_command_info.number_of_rules,
32+
lint_command_info.threads_count
33+
))
34+
}
35+
2436
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
2537
Box::new(GraphicalReporter::default())
2638
}
2739
}
2840

41+
impl DefaultOutputFormatter {
42+
fn get_execution_time(duration: &Duration) -> String {
43+
let ms = duration.as_millis();
44+
if ms < 1000 {
45+
format!("{ms}ms")
46+
} else {
47+
format!("{:.1}s", duration.as_secs_f64())
48+
}
49+
}
50+
}
51+
2952
/// Pretty-prints diagnostics. Primarily meant for human-readable output in a terminal.
3053
///
3154
/// See [`GraphicalReportHandler`] for how to configure colors, context lines, etc.
@@ -40,8 +63,35 @@ impl Default for GraphicalReporter {
4063
}
4164

4265
impl DiagnosticReporter for GraphicalReporter {
43-
fn finish(&mut self, _: &DiagnosticResult) -> Option<String> {
44-
None
66+
fn finish(&mut self, result: &DiagnosticResult) -> Option<String> {
67+
let mut output = String::new();
68+
69+
if result.warnings_count() + result.errors_count() > 0 {
70+
output.push('\n');
71+
}
72+
73+
output.push_str(
74+
format!(
75+
"Found {} warning{} and {} error{}.\n",
76+
result.warnings_count(),
77+
if result.warnings_count() == 1 { "" } else { "s" },
78+
result.errors_count(),
79+
if result.errors_count() == 1 { "" } else { "s" },
80+
)
81+
.as_str(),
82+
);
83+
84+
if result.max_warnings_exceeded() {
85+
output.push_str(
86+
format!(
87+
"Exceeded maximum number of warnings. Found {}.\n",
88+
result.warnings_count()
89+
)
90+
.as_str(),
91+
);
92+
}
93+
94+
Some(output)
4595
}
4696

4797
fn render_error(&mut self, error: Error) -> Option<String> {
@@ -60,9 +110,11 @@ impl GraphicalReporter {
60110

61111
#[cfg(test)]
62112
mod test {
113+
use std::time::Duration;
114+
63115
use crate::output_formatter::{
64116
default::{DefaultOutputFormatter, GraphicalReporter},
65-
InternalFormatter,
117+
InternalFormatter, LintCommandInfo,
66118
};
67119
use miette::{GraphicalReportHandler, GraphicalTheme, NamedSource};
68120
use oxc_diagnostics::{
@@ -81,12 +133,63 @@ mod test {
81133
}
82134

83135
#[test]
84-
fn reporter_finish() {
136+
fn lint_command_info() {
137+
let formatter = DefaultOutputFormatter;
138+
let result = formatter.lint_command_info(&LintCommandInfo {
139+
number_of_files: 5,
140+
number_of_rules: 10,
141+
threads_count: 12,
142+
start_time: Duration::new(1, 0),
143+
});
144+
145+
assert!(result.is_some());
146+
assert_eq!(
147+
result.unwrap(),
148+
"Finished in 1.0s on 5 files with 10 rules using 12 threads.\n"
149+
);
150+
}
151+
152+
#[test]
153+
fn reporter_finish_no_results() {
85154
let mut reporter = GraphicalReporter::default();
86155

87156
let result = reporter.finish(&DiagnosticResult::default());
88157

89-
assert!(result.is_none());
158+
assert!(result.is_some());
159+
assert_eq!(result.unwrap(), "Found 0 warnings and 0 errors.\n");
160+
}
161+
162+
#[test]
163+
fn reporter_finish_one_warning_and_one_error() {
164+
let mut reporter = GraphicalReporter::default();
165+
166+
let result = reporter.finish(&DiagnosticResult::new(1, 1, false));
167+
168+
assert!(result.is_some());
169+
assert_eq!(result.unwrap(), "\nFound 1 warning and 1 error.\n");
170+
}
171+
172+
#[test]
173+
fn reporter_finish_multiple_warning_and_errors() {
174+
let mut reporter = GraphicalReporter::default();
175+
176+
let result = reporter.finish(&DiagnosticResult::new(6, 4, false));
177+
178+
assert!(result.is_some());
179+
assert_eq!(result.unwrap(), "\nFound 6 warnings and 4 errors.\n");
180+
}
181+
182+
#[test]
183+
fn reporter_finish_exceeded_warnings() {
184+
let mut reporter = GraphicalReporter::default();
185+
186+
let result = reporter.finish(&DiagnosticResult::new(6, 4, true));
187+
188+
assert!(result.is_some());
189+
assert_eq!(
190+
result.unwrap(),
191+
"\nFound 6 warnings and 4 errors.\nExceeded maximum number of warnings. Found 6.\n"
192+
);
90193
}
91194

92195
#[test]

Diff for: ‎apps/oxlint/src/output_formatter/json.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ fn format_json(diagnostics: &mut Vec<Error>) -> String {
7676
})
7777
.collect::<Vec<_>>()
7878
.join(",\n");
79-
format!("[\n{messages}\n]")
79+
format!("[\n{messages}\n]\n")
8080
}
8181

8282
#[cfg(test)]
@@ -108,7 +108,7 @@ mod test {
108108
assert!(second_result.is_some());
109109
assert_eq!(
110110
second_result.unwrap(),
111-
"[\n\t{\"message\": \"error message\",\"severity\": \"warning\",\"causes\": [],\"filename\": \"file://test.ts\",\"labels\": [{\"span\": {\"offset\": 0,\"length\": 8}}],\"related\": []}\n]"
111+
"[\n\t{\"message\": \"error message\",\"severity\": \"warning\",\"causes\": [],\"filename\": \"file://test.ts\",\"labels\": [{\"span\": {\"offset\": 0,\"length\": 8}}],\"related\": []}\n]\n"
112112
);
113113
}
114114
}

Diff for: ‎apps/oxlint/src/output_formatter/mod.rs

+40-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod unix;
77

88
use std::io::{BufWriter, Stdout, Write};
99
use std::str::FromStr;
10+
use std::time::Duration;
1011

1112
use checkstyle::CheckStyleOutputFormatter;
1213
use github::GithubOutputFormatter;
@@ -45,19 +46,45 @@ impl FromStr for OutputFormat {
4546
}
4647
}
4748

49+
/// Some extra lint information, which can be outputted
50+
/// at the end of the command
51+
pub struct LintCommandInfo {
52+
/// The number of files that were linted.
53+
pub number_of_files: usize,
54+
/// The number of lint rules that were run.
55+
pub number_of_rules: usize,
56+
/// The used CPU threads count
57+
pub threads_count: usize,
58+
/// Some reporters want to output the duration it took to finished the task
59+
pub start_time: Duration,
60+
}
61+
62+
/// An Interface for the different output formats.
63+
/// The Formatter is then managed by [`OutputFormatter`].
4864
trait InternalFormatter {
65+
/// Print all available rules by oxlint
66+
/// Some Formatter do not know how to output the rules in the style,
67+
/// instead you should print out that this combination of flags is not supported.
68+
/// Example: "flag --rules with flag --format=checkstyle is not allowed"
4969
fn all_rules(&mut self, writer: &mut dyn Write);
5070

71+
/// At the end of the Lint command the Formatter can output extra information.
72+
fn lint_command_info(&self, _lint_command_info: &LintCommandInfo) -> Option<String> {
73+
None
74+
}
75+
76+
/// oxlint words with [`DiagnosticService`](oxc_diagnostics::DiagnosticService),
77+
/// which uses a own reporter to output to stdout.
5178
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
5279
}
5380

5481
pub struct OutputFormatter {
55-
internal_formatter: Box<dyn InternalFormatter>,
82+
internal: Box<dyn InternalFormatter>,
5683
}
5784

5885
impl OutputFormatter {
5986
pub fn new(format: OutputFormat) -> Self {
60-
Self { internal_formatter: Self::get_internal_formatter(format) }
87+
Self { internal: Self::get_internal_formatter(format) }
6188
}
6289

6390
fn get_internal_formatter(format: OutputFormat) -> Box<dyn InternalFormatter> {
@@ -71,11 +98,20 @@ impl OutputFormatter {
7198
}
7299
}
73100

101+
/// Print all available rules by oxlint
102+
/// See [`InternalFormatter::all_rules`] for more details.
74103
pub fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
75-
self.internal_formatter.all_rules(writer);
104+
self.internal.all_rules(writer);
105+
}
106+
107+
/// At the end of the Lint command we may output extra information.
108+
pub fn lint_command_info(&self, lint_command_info: &LintCommandInfo) -> Option<String> {
109+
self.internal.lint_command_info(lint_command_info)
76110
}
77111

112+
/// Returns the [`DiagnosticReporter`] which then will be used by [`DiagnosticService`](oxc_diagnostics::DiagnosticService)
113+
/// See [`InternalFormatter::get_diagnostic_reporter`] for more details.
78114
pub fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
79-
self.internal_formatter.get_diagnostic_reporter()
115+
self.internal.get_diagnostic_reporter()
80116
}
81117
}

Diff for: ‎apps/oxlint/src/result.rs

+7-63
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::{
22
path::PathBuf,
33
process::{ExitCode, Termination},
4-
time::Duration,
54
};
65

76
#[derive(Debug)]
@@ -17,22 +16,14 @@ pub enum CliRunResult {
1716
/// A summary of a complete linter run.
1817
#[derive(Debug, Default)]
1918
pub struct LintResult {
20-
/// The total time it took to run the linter.
21-
pub duration: Duration,
22-
/// The number of lint rules that were run.
23-
pub number_of_rules: usize,
2419
/// The number of files that were linted.
2520
pub number_of_files: usize,
2621
/// The number of warnings that were found.
2722
pub number_of_warnings: usize,
2823
/// The number of errors that were found.
2924
pub number_of_errors: usize,
30-
/// Whether or not the maximum number of warnings was exceeded.
31-
pub max_warnings_exceeded: bool,
32-
/// Whether or not warnings should be treated as errors (from `--deny-warnings` for example)
33-
pub deny_warnings: bool,
34-
/// Whether or not to print a summary of the results
35-
pub print_summary: bool,
25+
/// The exit unix code for, in general 0 or 1 (from `--deny-warnings` or `--max-warnings` for example)
26+
pub exit_code: ExitCode,
3627
}
3728

3829
impl Termination for CliRunResult {
@@ -49,47 +40,11 @@ impl Termination for CliRunResult {
4940
ExitCode::from(1)
5041
}
5142
Self::LintResult(LintResult {
52-
duration,
53-
number_of_rules,
54-
number_of_files,
55-
number_of_warnings,
56-
number_of_errors,
57-
max_warnings_exceeded,
58-
deny_warnings,
59-
print_summary,
60-
}) => {
61-
if print_summary {
62-
let threads = rayon::current_num_threads();
63-
let number_of_diagnostics = number_of_warnings + number_of_errors;
64-
65-
if number_of_diagnostics > 0 {
66-
println!();
67-
}
68-
69-
let time = Self::get_execution_time(&duration);
70-
let s = if number_of_files == 1 { "" } else { "s" };
71-
println!(
72-
"Finished in {time} on {number_of_files} file{s} with {number_of_rules} rules using {threads} threads."
73-
);
74-
75-
if max_warnings_exceeded {
76-
println!(
77-
"Exceeded maximum number of warnings. Found {number_of_warnings}."
78-
);
79-
return ExitCode::from(1);
80-
}
81-
82-
println!(
83-
"Found {number_of_warnings} warning{} and {number_of_errors} error{}.",
84-
if number_of_warnings == 1 { "" } else { "s" },
85-
if number_of_errors == 1 { "" } else { "s" }
86-
);
87-
}
88-
89-
let exit_code =
90-
u8::from((number_of_warnings > 0 && deny_warnings) || number_of_errors > 0);
91-
ExitCode::from(exit_code)
92-
}
43+
number_of_files: _, // ToDo: only for tests, make snapshots
44+
number_of_warnings: _, // ToDo: only for tests, make snapshots
45+
number_of_errors: _,
46+
exit_code,
47+
}) => exit_code,
9348
Self::PrintConfigResult { config_file } => {
9449
println!("{config_file}");
9550
ExitCode::from(0)
@@ -101,14 +56,3 @@ impl Termination for CliRunResult {
10156
}
10257
}
10358
}
104-
105-
impl CliRunResult {
106-
fn get_execution_time(duration: &Duration) -> String {
107-
let ms = duration.as_millis();
108-
if ms < 1000 {
109-
format!("{ms}ms")
110-
} else {
111-
format!("{:.1}s", duration.as_secs_f64())
112-
}
113-
}
114-
}

Diff for: ‎crates/oxc_diagnostics/src/service.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ impl DiagnosticService {
220220
}
221221

222222
fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> {
223-
// Do not panic when the process is skill (e.g. piping into `less`).
223+
// Do not panic when the process is killed (e.g. piping into `less`).
224224
if matches!(error.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) {
225225
Ok(())
226226
} else {

0 commit comments

Comments
 (0)
Please sign in to comment.