summaryrefslogtreecommitdiff
path: root/cli/tools/lint/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/lint/mod.rs')
-rw-r--r--cli/tools/lint/mod.rs195
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)
+ }
_ => (),
}