diff options
-rw-r--r-- | cli/flags.rs | 36 | ||||
-rw-r--r-- | cli/lint.rs | 168 | ||||
-rw-r--r-- | cli/main.rs | 6 | ||||
-rw-r--r-- | cli/tests/integration_tests.rs | 11 | ||||
-rw-r--r-- | cli/tests/lint/expected_ignore.out | 2 | ||||
-rw-r--r-- | cli/tests/lint/expected_json.out | 43 | ||||
-rw-r--r-- | cli/tests/lint/malformed.js | 4 |
7 files changed, 233 insertions, 37 deletions
diff --git a/cli/flags.rs b/cli/flags.rs index 6c58238de..537e768f2 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -57,6 +57,7 @@ pub enum DenoSubcommand { files: Vec<String>, ignore: Vec<String>, rules: bool, + json: bool, }, Repl, Run { @@ -636,10 +637,12 @@ fn lint_parse(flags: &mut Flags, matches: &clap::ArgMatches) { None => vec![], }; let rules = matches.is_present("rules"); + let json = matches.is_present("json"); flags.subcommand = DenoSubcommand::Lint { files, rules, ignore, + json, }; } @@ -1008,6 +1011,9 @@ fn lint_subcommand<'a, 'b>() -> App<'a, 'b> { deno lint --unstable deno lint --unstable myfile1.ts myfile2.js +Print result as JSON: + deno lint --unstable --json + List available rules: deno lint --unstable --rules @@ -1042,6 +1048,12 @@ Ignore linting a file by adding an ignore comment at the top of the file: .help("Ignore linting particular source files."), ) .arg( + Arg::with_name("json") + .long("json") + .help("Output lint result in JSON format.") + .takes_value(false), + ) + .arg( Arg::with_name("files") .takes_value(true) .multiple(true) @@ -1761,6 +1773,7 @@ mod tests { subcommand: DenoSubcommand::Lint { files: vec!["script_1.ts".to_string(), "script_2.ts".to_string()], rules: false, + json: false, ignore: vec![], }, unstable: true, @@ -1780,6 +1793,7 @@ mod tests { subcommand: DenoSubcommand::Lint { files: vec![], rules: false, + json: false, ignore: svec!["script_1.ts", "script_2.ts"], }, unstable: true, @@ -1794,6 +1808,28 @@ mod tests { subcommand: DenoSubcommand::Lint { files: vec![], rules: true, + json: false, + ignore: vec![], + }, + unstable: true, + ..Flags::default() + } + ); + + let r = flags_from_vec_safe(svec![ + "deno", + "lint", + "--unstable", + "--json", + "script_1.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Lint { + files: vec!["script_1.ts".to_string()], + rules: false, + json: true, ignore: vec![], }, unstable: true, diff --git a/cli/lint.rs b/cli/lint.rs index 3e9380d9d..bf6dc16b6 100644 --- a/cli/lint.rs +++ b/cli/lint.rs @@ -6,7 +6,6 @@ //! At the moment it is only consumed using CLI but in //! the future it can be easily extended to provide //! the same functions as ops available in JS runtime. - use crate::colors; use crate::file_fetcher::map_file_extension; use crate::fmt::collect_files; @@ -19,15 +18,29 @@ use deno_lint::linter::Linter; use deno_lint::linter::LinterBuilder; use deno_lint::rules; use deno_lint::rules::LintRule; +use serde::Serialize; use std::fs; use std::path::PathBuf; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use swc_ecmascript::parser::Syntax; +pub enum LintReporterKind { + Pretty, + Json, +} + +fn create_reporter(kind: LintReporterKind) -> Box<dyn LintReporter + Send> { + match kind { + LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()), + LintReporterKind::Json => Box::new(JsonLintReporter::new()), + } +} + pub async fn lint_files( args: Vec<String>, ignore: Vec<String>, + json: bool, ) -> Result<(), ErrBox> { let mut target_files = collect_files(args)?; if !ignore.is_empty() { @@ -38,28 +51,32 @@ pub async fn lint_files( } debug!("Found {} files", target_files.len()); - let error_count = Arc::new(AtomicUsize::new(0)); + let has_error = Arc::new(AtomicBool::new(false)); - // prevent threads outputting at the same time - let output_lock = Arc::new(Mutex::new(0)); + let reporter_kind = if json { + LintReporterKind::Json + } else { + LintReporterKind::Pretty + }; + let reporter_lock = Arc::new(Mutex::new(create_reporter(reporter_kind))); run_parallelized(target_files, { - let error_count = error_count.clone(); + let reporter_lock = reporter_lock.clone(); + let has_error = has_error.clone(); move |file_path| { let r = lint_file(file_path.clone()); + let mut reporter = reporter_lock.lock().unwrap(); match r { Ok(file_diagnostics) => { - error_count.fetch_add(file_diagnostics.len(), Ordering::SeqCst); - let _g = output_lock.lock().unwrap(); for d in file_diagnostics.iter() { - let fmt_diagnostic = format_diagnostic(d); - eprintln!("{}\n", fmt_diagnostic); + has_error.store(true, Ordering::Relaxed); + reporter.visit(&d); } } Err(err) => { - eprintln!("Error linting: {}", file_path.to_string_lossy()); - eprintln!(" {}", err); + has_error.store(true, Ordering::Relaxed); + reporter.visit_error(&file_path.to_string_lossy().to_string(), &err); } } Ok(()) @@ -67,9 +84,11 @@ pub async fn lint_files( }) .await?; - let error_count = error_count.load(Ordering::SeqCst); - if error_count > 0 { - eprintln!("Found {} problems", error_count); + let has_error = has_error.load(Ordering::Relaxed); + + reporter_lock.lock().unwrap().close(); + + if has_error { std::process::exit(1); } @@ -170,21 +189,106 @@ fn lint_file(file_path: PathBuf) -> Result<Vec<LintDiagnostic>, ErrBox> { Ok(file_diagnostics) } -fn format_diagnostic(d: &LintDiagnostic) -> String { - let pretty_message = - format!("({}) {}", colors::gray(&d.code), d.message.clone()); - - fmt_errors::format_stack( - true, - &pretty_message, - Some(&d.line_src), - Some(d.location.col as i64), - Some((d.location.col + d.snippet_length) as i64), - &[fmt_errors::format_location( - &d.location.filename, - d.location.line as i64, - d.location.col as i64, - )], - 0, - ) +trait LintReporter { + fn visit(&mut self, d: &LintDiagnostic); + fn visit_error(&mut self, file_path: &str, err: &ErrBox); + fn close(&mut self); +} + +#[derive(Serialize)] +struct LintError { + file_path: String, + message: String, +} + +struct PrettyLintReporter { + lint_count: u32, +} + +impl PrettyLintReporter { + fn new() -> PrettyLintReporter { + PrettyLintReporter { lint_count: 0 } + } +} + +impl LintReporter for PrettyLintReporter { + fn visit(&mut self, d: &LintDiagnostic) { + self.lint_count += 1; + + let pretty_message = + format!("({}) {}", colors::gray(&d.code), d.message.clone()); + + let message = fmt_errors::format_stack( + true, + &pretty_message, + Some(&d.line_src), + Some(d.location.col as i64), + Some((d.location.col + d.snippet_length) as i64), + &[fmt_errors::format_location( + &d.location.filename, + d.location.line as i64, + d.location.col as i64, + )], + 0, + ); + + eprintln!("{}\n", message); + } + + fn visit_error(&mut self, file_path: &str, err: &ErrBox) { + eprintln!("Error linting: {}", file_path); + eprintln!(" {}", err); + } + + fn close(&mut self) { + match self.lint_count { + 1 => eprintln!("Found 1 problem"), + n if n > 1 => eprintln!("Found {} problems", self.lint_count), + _ => (), + } + } +} + +#[derive(Serialize)] +struct JsonLintReporter { + diagnostics: Vec<LintDiagnostic>, + errors: Vec<LintError>, +} + +impl JsonLintReporter { + fn new() -> JsonLintReporter { + JsonLintReporter { + diagnostics: Vec::new(), + errors: Vec::new(), + } + } +} + +impl LintReporter for JsonLintReporter { + fn visit(&mut self, d: &LintDiagnostic) { + self.diagnostics.push(d.clone()); + } + + fn visit_error(&mut self, file_path: &str, err: &ErrBox) { + self.errors.push(LintError { + file_path: file_path.to_string(), + message: err.to_string(), + }); + } + + fn close(&mut self) { + // Sort so that we guarantee a deterministic output which is useful for tests + self + .diagnostics + .sort_by(|a, b| get_sort_key(&a).cmp(&get_sort_key(&b))); + + let json = serde_json::to_string_pretty(&self); + eprintln!("{}", json.unwrap()); + } +} + +pub fn get_sort_key(a: &LintDiagnostic) -> String { + let location = &a.location; + + return format!("{}:{}:{}", location.filename, location.line, location.col); } diff --git a/cli/main.rs b/cli/main.rs index 27948e14f..b5eb0b745 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -346,6 +346,7 @@ async fn lint_command( files: Vec<String>, list_rules: bool, ignore: Vec<String>, + json: bool, ) -> Result<(), ErrBox> { if !flags.unstable { exit_unstable("lint"); @@ -356,7 +357,7 @@ async fn lint_command( return Ok(()); } - lint::lint_files(files, ignore).await + lint::lint_files(files, ignore, json).await } async fn cache_command(flags: Flags, files: Vec<String>) -> Result<(), ErrBox> { @@ -738,7 +739,8 @@ pub fn main() { files, rules, ignore, - } => lint_command(flags, files, rules, ignore).boxed_local(), + json, + } => lint_command(flags, files, rules, ignore, json).boxed_local(), DenoSubcommand::Repl => run_repl(flags).boxed_local(), DenoSubcommand::Run { script } => run_command(flags, script).boxed_local(), DenoSubcommand::Test { diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index bb7ba0d6f..6e487d12f 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -2216,14 +2216,21 @@ itest!(deno_lint { exit_code: 1, }); +itest!(deno_lint_json { + args: + "lint --unstable --json lint/file1.js lint/file2.ts lint/ignored_file.ts lint/malformed.js", + output: "lint/expected_json.out", + exit_code: 1, +}); + itest!(deno_lint_ignore { - args: "lint --unstable --ignore=lint/file1.js lint/", + args: "lint --unstable --ignore=lint/file1.js,lint/malformed.js lint/", output: "lint/expected_ignore.out", exit_code: 1, }); itest!(deno_lint_glob { - args: "lint --unstable lint/", + args: "lint --unstable --ignore=lint/malformed.js lint/", output: "lint/expected_glob.out", exit_code: 1, }); diff --git a/cli/tests/lint/expected_ignore.out b/cli/tests/lint/expected_ignore.out index 6041d1c6a..02b9d917c 100644 --- a/cli/tests/lint/expected_ignore.out +++ b/cli/tests/lint/expected_ignore.out @@ -1,2 +1,2 @@ [WILDCARD] -Found 1 problems +Found 1 problem diff --git a/cli/tests/lint/expected_json.out b/cli/tests/lint/expected_json.out new file mode 100644 index 000000000..b4ce63395 --- /dev/null +++ b/cli/tests/lint/expected_json.out @@ -0,0 +1,43 @@ +{ + "diagnostics": [ + { + "location": { + "filename": "[WILDCARD]", + "line": 1, + "col": 0 + }, + "message": "Ignore directive requires lint rule code", + "code": "ban-untagged-ignore", + "line_src": "// deno-lint-ignore", + "snippet_length": 19 + }, + { + "location": { + "filename": "[WILDCARD]", + "line": 2, + "col": 14 + }, + "message": "Empty block statement", + "code": "no-empty", + "line_src": "while (false) {}", + "snippet_length": 2 + }, + { + "location": { + "filename": "[WILDCARD]", + "line": 3, + "col": 12 + }, + "message": "Empty block statement", + "code": "no-empty", + "line_src": "} catch (e) {}", + "snippet_length": 2 + } + ], + "errors": [ + { + "file_path": "[WILDCARD]malformed.js", + "message": "Expected RBrace, got None at [WILDCARD]malformed.js:4:15" + } + ] +} diff --git a/cli/tests/lint/malformed.js b/cli/tests/lint/malformed.js new file mode 100644 index 000000000..5ad4650d6 --- /dev/null +++ b/cli/tests/lint/malformed.js @@ -0,0 +1,4 @@ +// deno-fmt-ignore-file + +// intentionally malformed file +export class A {
\ No newline at end of file |