diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2024-03-21 14:18:59 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-21 14:18:59 -0700 |
commit | ffbcad3800ef086bad791c1c640b62fd72d60172 (patch) | |
tree | f350a54862928e19ba93a75b71a4c4bebcc974f3 /cli/tools/lint | |
parent | 2166aa8fb6be5fdd6d607db587e236de11b6fb91 (diff) |
feat(lint): `deno lint --fix` and lsp quick fixes (#22615)
Adds a `--fix` option to deno lint. This currently doesn't work for
basically any rules, but we can add them over time to deno lint.
Diffstat (limited to 'cli/tools/lint')
-rw-r--r-- | cli/tools/lint/mod.rs | 195 |
1 files changed, 170 insertions, 25 deletions
diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs index 251efd941..bf96eca06 100644 --- a/cli/tools/lint/mod.rs +++ b/cli/tools/lint/mod.rs @@ -10,6 +10,7 @@ use deno_ast::SourceRange; use deno_ast::SourceTextInfo; use deno_config::glob::FilePatterns; use deno_core::anyhow::bail; +use deno_core::anyhow::Context; use deno_core::error::generic_error; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; @@ -216,9 +217,10 @@ async fn lint_files( futures.push({ let has_error = has_error.clone(); - let lint_rules = lint_rules.rules.clone(); + let linter = create_linter(lint_rules.rules); let reporter_lock = reporter_lock.clone(); let incremental_cache = incremental_cache.clone(); + let fix = lint_options.fix; deno_core::unsync::spawn(async move { run_parallelized(paths, { move |file_path| { @@ -229,12 +231,15 @@ async fn lint_files( return Ok(()); } - let r = lint_file(&file_path, file_text, lint_rules); - if let Ok((file_diagnostics, file_source)) = &r { + let r = lint_file(&linter, &file_path, file_text, fix); + if let Ok((file_source, file_diagnostics)) = &r { if file_diagnostics.is_empty() { // update the incremental cache if there were no diagnostics - incremental_cache - .update_file(&file_path, file_source.text_info().text_str()) + incremental_cache.update_file( + &file_path, + // ensure the returned text is used here as it may have been modified via --fix + file_source.text_info().text_str(), + ) } } @@ -322,22 +327,145 @@ pub fn create_linter(rules: Vec<&'static dyn LintRule>) -> Linter { } fn lint_file( + linter: &Linter, file_path: &Path, source_code: String, - lint_rules: Vec<&'static dyn LintRule>, -) -> Result<(Vec<LintDiagnostic>, ParsedSource), AnyError> { + fix: bool, +) -> Result<(ParsedSource, Vec<LintDiagnostic>), AnyError> { let specifier = specifier_from_file_path(file_path)?; let media_type = MediaType::from_specifier(&specifier); - let linter = create_linter(lint_rules); + if fix { + lint_file_and_fix(linter, &specifier, media_type, source_code, file_path) + } else { + linter + .lint_file(LintFileOptions { + specifier, + media_type, + source_code, + }) + .map_err(AnyError::from) + } +} - let (source, file_diagnostics) = linter.lint_file(LintFileOptions { - specifier, +fn lint_file_and_fix( + linter: &Linter, + specifier: &ModuleSpecifier, + media_type: MediaType, + source_code: String, + file_path: &Path, +) -> Result<(ParsedSource, Vec<LintDiagnostic>), deno_core::anyhow::Error> { + // initial lint + let (source, diagnostics) = linter.lint_file(LintFileOptions { + specifier: specifier.clone(), media_type, - source_code: source_code.clone(), + source_code, })?; - Ok((file_diagnostics, source)) + // Try applying fixes repeatedly until the file has none left or + // a maximum number of iterations is reached. This is necessary + // because lint fixes may overlap and so we can't always apply + // them in one pass. + let mut source = source; + let mut diagnostics = diagnostics; + let mut fix_iterations = 0; + loop { + let change = apply_lint_fixes_and_relint( + specifier, + media_type, + linter, + source.text_info(), + &diagnostics, + )?; + match change { + Some(change) => { + source = change.0; + diagnostics = change.1; + } + None => { + break; + } + } + fix_iterations += 1; + if fix_iterations > 5 { + log::warn!( + concat!( + "Reached maximum number of fix iterations for '{}'. There's ", + "probably a bug in Deno. Please fix this file manually.", + ), + specifier, + ); + break; + } + } + + if fix_iterations > 0 { + // everything looks good and the file still parses, so write it out + fs::write(file_path, source.text_info().text_str()) + .context("Failed writing fix to file.")?; + } + + Ok((source, diagnostics)) +} + +fn apply_lint_fixes_and_relint( + specifier: &ModuleSpecifier, + media_type: MediaType, + linter: &Linter, + text_info: &SourceTextInfo, + diagnostics: &[LintDiagnostic], +) -> Result<Option<(ParsedSource, Vec<LintDiagnostic>)>, AnyError> { + let Some(new_text) = apply_lint_fixes(text_info, diagnostics) else { + return Ok(None); + }; + linter + .lint_file(LintFileOptions { + specifier: specifier.clone(), + source_code: new_text, + media_type, + }) + .map(Some) + .context( + "An applied lint fix caused a syntax error. Please report this bug.", + ) +} + +fn apply_lint_fixes( + text_info: &SourceTextInfo, + diagnostics: &[LintDiagnostic], +) -> Option<String> { + if diagnostics.is_empty() { + return None; + } + + let file_start = text_info.range().start; + let mut quick_fixes = diagnostics + .iter() + // use the first quick fix + .filter_map(|d| d.fixes.first()) + .flat_map(|fix| fix.changes.iter()) + .map(|change| deno_ast::TextChange { + range: change.range.as_byte_range(file_start), + new_text: change.new_text.to_string(), + }) + .collect::<Vec<_>>(); + if quick_fixes.is_empty() { + return None; + } + // remove any overlapping text changes, we'll circle + // back for another pass to fix the remaining + quick_fixes.sort_by_key(|change| change.range.start); + for i in (1..quick_fixes.len()).rev() { + let cur = &quick_fixes[i]; + let previous = &quick_fixes[i - 1]; + let is_overlapping = cur.range.start < previous.range.end; + if is_overlapping { + quick_fixes.remove(i); + } + } + let new_text = + deno_ast::apply_text_changes(text_info.text_str(), quick_fixes); + Some(new_text) } /// Lint stdin and write result to stdout. @@ -346,7 +474,7 @@ fn lint_file( fn lint_stdin( file_path: &Path, lint_rules: Vec<&'static dyn LintRule>, -) -> Result<(Vec<LintDiagnostic>, ParsedSource), AnyError> { +) -> Result<(ParsedSource, Vec<LintDiagnostic>), AnyError> { let mut source_code = String::new(); if stdin().read_to_string(&mut source_code).is_err() { return Err(generic_error("Failed to read from stdin")); @@ -354,24 +482,24 @@ fn lint_stdin( let linter = create_linter(lint_rules); - let (source, file_diagnostics) = linter.lint_file(LintFileOptions { - specifier: specifier_from_file_path(file_path)?, - source_code: source_code.clone(), - media_type: MediaType::TypeScript, - })?; - - Ok((file_diagnostics, source)) + linter + .lint_file(LintFileOptions { + specifier: specifier_from_file_path(file_path)?, + source_code: source_code.clone(), + media_type: MediaType::TypeScript, + }) + .map_err(AnyError::from) } fn handle_lint_result( file_path: &str, - result: Result<(Vec<LintDiagnostic>, ParsedSource), AnyError>, + result: Result<(ParsedSource, Vec<LintDiagnostic>), AnyError>, reporter_lock: Arc<Mutex<Box<dyn LintReporter + Send>>>, ) -> bool { let mut reporter = reporter_lock.lock(); match result { - Ok((mut file_diagnostics, _source)) => { + Ok((_source, mut file_diagnostics)) => { file_diagnostics.sort_by(|a, b| match a.specifier.cmp(&b.specifier) { std::cmp::Ordering::Equal => a.range.start.cmp(&b.range.start), file_order => file_order, @@ -493,17 +621,26 @@ struct LintError { struct PrettyLintReporter { lint_count: u32, + fixable_diagnostics: u32, } impl PrettyLintReporter { fn new() -> PrettyLintReporter { - PrettyLintReporter { lint_count: 0 } + PrettyLintReporter { + lint_count: 0, + fixable_diagnostics: 0, + } } } impl LintReporter for PrettyLintReporter { fn visit_diagnostic(&mut self, d: LintOrCliDiagnostic) { self.lint_count += 1; + if let LintOrCliDiagnostic::Lint(d) = d { + if !d.fixes.is_empty() { + self.fixable_diagnostics += 1; + } + } eprintln!("{}", d.display()); } @@ -514,9 +651,17 @@ impl LintReporter for PrettyLintReporter { } fn close(&mut self, check_count: usize) { + let fixable_suffix = if self.fixable_diagnostics > 0 { + colors::gray(format!(" ({} fixable via --fix)", self.fixable_diagnostics)) + .to_string() + } else { + "".to_string() + }; match self.lint_count { - 1 => info!("Found 1 problem"), - n if n > 1 => info!("Found {} problems", self.lint_count), + 1 => info!("Found 1 problem{}", fixable_suffix), + n if n > 1 => { + info!("Found {} problems{}", self.lint_count, fixable_suffix) + } _ => (), } |