From ffbcad3800ef086bad791c1c640b62fd72d60172 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Thu, 21 Mar 2024 14:18:59 -0700 Subject: [PATCH] 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. --- Cargo.lock | 4 +- cli/Cargo.toml | 2 +- cli/args/flags.rs | 27 ++- cli/args/mod.rs | 4 + cli/lsp/analysis.rs | 110 +++++++++- cli/lsp/diagnostics.rs | 1 + cli/lsp/language_server.rs | 2 +- cli/tools/lint/mod.rs | 195 +++++++++++++++--- tests/integration/lsp_tests.rs | 98 +++++++++ tests/specs/lint/lint_fix/__test__.jsonc | 17 ++ tests/specs/lint/lint_fix/a.ts | 4 + tests/specs/lint/lint_fix/a_fixed.out | 4 + tests/specs/lint/lint_fix/lint.out | 2 + tests/specs/lint/lint_fix/lint_fixed.out | 1 + tests/testdata/cat.ts | 12 +- tests/testdata/lint/expected_from_stdin.out | 2 +- tests/testdata/lint/expected_quiet.out | 4 +- tests/testdata/lint/with_config.out | 4 +- tests/testdata/lint/with_config_and_flags.out | 4 +- .../lint/with_config_without_tags.out | 4 +- 20 files changed, 442 insertions(+), 59 deletions(-) create mode 100644 tests/specs/lint/lint_fix/__test__.jsonc create mode 100644 tests/specs/lint/lint_fix/a.ts create mode 100644 tests/specs/lint/lint_fix/a_fixed.out create mode 100644 tests/specs/lint/lint_fix/lint.out create mode 100644 tests/specs/lint/lint_fix/lint_fixed.out diff --git a/Cargo.lock b/Cargo.lock index d82e9ac598..50bc478c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1554,9 +1554,9 @@ dependencies = [ [[package]] name = "deno_lint" -version = "0.57.1" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96b203021bf8c738b37e6d3e792e9b04ed61ed4b9204426bf29ed637ccb9ed0" +checksum = "3344701fc527a920b4f00562052d3feca8ce315bb9327305b07d5820019a9ead" dependencies = [ "anyhow", "deno_ast", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a0d46ebc2f..939dfee74f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -69,7 +69,7 @@ deno_core = { workspace = true, features = ["include_js_files_for_snapshotting"] deno_doc = { version = "=0.113.1", features = ["html"] } deno_emit = "=0.38.2" deno_graph = { version = "=0.69.9", features = ["tokio_executor"] } -deno_lint = { version = "=0.57.1", features = ["docs"] } +deno_lint = { version = "=0.58.0", features = ["docs"] } deno_lockfile.workspace = true deno_npm = "=0.17.0" deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] } diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 07b60331ed..22c7d8e6d6 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -199,6 +199,7 @@ pub struct UninstallFlags { pub struct LintFlags { pub files: FileFlags, pub rules: bool, + pub fix: bool, pub maybe_rules_tags: Option>, pub maybe_rules_include: Option>, pub maybe_rules_exclude: Option>, @@ -2005,6 +2006,12 @@ Ignore linting a file by adding an ignore comment at the top of the file: ) .defer(|cmd| { cmd + .arg( + Arg::new("fix") + .long("fix") + .help("Fix any linting errors for rules that support it") + .action(ArgAction::SetTrue), + ) .arg( Arg::new("rules") .long("rules") @@ -3622,6 +3629,7 @@ fn lint_parse(flags: &mut Flags, matches: &mut ArgMatches) { Some(f) => f.collect(), None => vec![], }; + let fix = matches.get_flag("fix"); let rules = matches.get_flag("rules"); let maybe_rules_tags = matches .remove_many::("rules-tags") @@ -3642,6 +3650,7 @@ fn lint_parse(flags: &mut Flags, matches: &mut ArgMatches) { include: files, ignore, }, + fix, rules, maybe_rules_tags, maybe_rules_include, @@ -5015,6 +5024,7 @@ mod tests { include: vec!["script_1.ts".to_string(), "script_2.ts".to_string(),], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5042,6 +5052,7 @@ mod tests { include: vec!["script_1.ts".to_string(), "script_2.ts".to_string()], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5070,6 +5081,7 @@ mod tests { include: vec!["script_1.ts".to_string(), "script_2.ts".to_string()], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5085,8 +5097,12 @@ mod tests { } ); - let r = - flags_from_vec(svec!["deno", "lint", "--ignore=script_1.ts,script_2.ts"]); + let r = flags_from_vec(svec![ + "deno", + "lint", + "--fix", + "--ignore=script_1.ts,script_2.ts" + ]); assert_eq!( r.unwrap(), Flags { @@ -5095,6 +5111,7 @@ mod tests { include: vec![], ignore: vec!["script_1.ts".to_string(), "script_2.ts".to_string()], }, + fix: true, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5116,6 +5133,7 @@ mod tests { include: vec![], ignore: vec![], }, + fix: false, rules: true, maybe_rules_tags: None, maybe_rules_include: None, @@ -5142,6 +5160,7 @@ mod tests { include: vec![], ignore: vec![], }, + fix: false, rules: true, maybe_rules_tags: Some(svec!["recommended"]), maybe_rules_include: None, @@ -5169,6 +5188,7 @@ mod tests { include: vec![], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: Some(svec![""]), maybe_rules_include: Some(svec!["ban-untagged-todo", "no-undef"]), @@ -5190,6 +5210,7 @@ mod tests { include: vec!["script_1.ts".to_string()], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5218,6 +5239,7 @@ mod tests { include: vec!["script_1.ts".to_string()], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, @@ -5247,6 +5269,7 @@ mod tests { include: vec!["script_1.ts".to_string()], ignore: vec![], }, + fix: false, rules: false, maybe_rules_tags: None, maybe_rules_include: None, diff --git a/cli/args/mod.rs b/cli/args/mod.rs index d957b8f80e..cb4473ca26 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -391,6 +391,7 @@ pub struct LintOptions { pub rules: LintRulesConfig, pub files: FilePatterns, pub reporter_kind: LintReporterKind, + pub fix: bool, } impl LintOptions { @@ -399,6 +400,7 @@ impl LintOptions { rules: Default::default(), files: FilePatterns::new_with_base(base), reporter_kind: Default::default(), + fix: false, } } @@ -407,6 +409,7 @@ impl LintOptions { maybe_lint_flags: Option, initial_cwd: &Path, ) -> Result { + let fix = maybe_lint_flags.as_ref().map(|f| f.fix).unwrap_or(false); let mut maybe_reporter_kind = maybe_lint_flags.as_ref().and_then(|lint_flags| { if lint_flags.json { @@ -464,6 +467,7 @@ impl LintOptions { maybe_rules_include, maybe_rules_exclude, ), + fix, }) } } diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 71f07275ef..54073edaf3 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -19,6 +19,7 @@ use deno_core::anyhow::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::ModuleSpecifier; @@ -78,6 +79,19 @@ static IMPORT_SPECIFIER_RE: Lazy = const SUPPORTED_EXTENSIONS: &[&str] = &[".ts", ".tsx", ".js", ".jsx", ".mjs"]; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DataQuickFixChange { + pub range: Range, + pub new_text: String, +} + +/// A quick fix that's stored in the diagnostic's data field. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DataQuickFix { + pub description: String, + pub changes: Vec, +} + /// Category of self-generated diagnostic messages (those not coming from) /// TypeScript. #[derive(Debug, PartialEq, Eq)] @@ -87,6 +101,7 @@ pub enum Category { message: String, code: String, hint: Option, + quick_fixes: Vec, }, } @@ -104,6 +119,7 @@ impl Reference { message, code, hint, + quick_fixes, } => lsp::Diagnostic { range: self.range, severity: Some(lsp::DiagnosticSeverity::WARNING), @@ -120,19 +136,26 @@ impl Reference { }, related_information: None, tags: None, // we should tag unused code - data: None, + data: if quick_fixes.is_empty() { + None + } else { + serde_json::to_value(quick_fixes).ok() + }, }, } } } -fn as_lsp_range(diagnostic: &LintDiagnostic) -> Range { - let start_lc = diagnostic - .text_info - .line_and_column_index(diagnostic.range.start); - let end_lc = diagnostic - .text_info - .line_and_column_index(diagnostic.range.end); +fn as_lsp_range_from_diagnostic(diagnostic: &LintDiagnostic) -> Range { + as_lsp_range(diagnostic.range, &diagnostic.text_info) +} + +fn as_lsp_range( + source_range: SourceRange, + text_info: &SourceTextInfo, +) -> Range { + let start_lc = text_info.line_and_column_index(source_range.start); + let end_lc = text_info.line_and_column_index(source_range.end); Range { start: Position { line: start_lc.line_index as u32, @@ -156,11 +179,26 @@ pub fn get_lint_references( lint_diagnostics .into_iter() .map(|d| Reference { - range: as_lsp_range(&d), + range: as_lsp_range_from_diagnostic(&d), category: Category::Lint { message: d.message, code: d.code, hint: d.hint, + quick_fixes: d + .fixes + .into_iter() + .map(|f| DataQuickFix { + description: f.description.to_string(), + changes: f + .changes + .into_iter() + .map(|change| DataQuickFixChange { + range: as_lsp_range(change.range, &d.text_info), + new_text: change.new_text.to_string(), + }) + .collect(), + }) + .collect(), }, }) .collect(), @@ -668,7 +706,57 @@ impl CodeActionCollection { Ok(()) } - pub fn add_deno_lint_ignore_action( + pub fn add_deno_lint_actions( + &mut self, + specifier: &ModuleSpecifier, + diagnostic: &lsp::Diagnostic, + maybe_text_info: Option, + maybe_parsed_source: Option, + ) -> Result<(), AnyError> { + if let Some(data_quick_fixes) = diagnostic + .data + .as_ref() + .and_then(|d| serde_json::from_value::>(d.clone()).ok()) + { + for quick_fix in data_quick_fixes { + let mut changes = HashMap::new(); + changes.insert( + specifier.clone(), + quick_fix + .changes + .into_iter() + .map(|change| lsp::TextEdit { + new_text: change.new_text.clone(), + range: change.range, + }) + .collect(), + ); + let code_action = lsp::CodeAction { + title: quick_fix.description.to_string(), + kind: Some(lsp::CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + command: None, + is_preferred: None, + disabled: None, + data: None, + edit: Some(lsp::WorkspaceEdit { + changes: Some(changes), + change_annotations: None, + document_changes: None, + }), + }; + self.actions.push(CodeActionKind::DenoLint(code_action)); + } + } + self.add_deno_lint_ignore_action( + specifier, + diagnostic, + maybe_text_info, + maybe_parsed_source, + ) + } + + fn add_deno_lint_ignore_action( &mut self, specifier: &ModuleSpecifier, diagnostic: &lsp::Diagnostic, @@ -1087,6 +1175,7 @@ mod tests { message: "message1".to_string(), code: "code1".to_string(), hint: None, + quick_fixes: Vec::new(), }, range, }, @@ -1105,6 +1194,7 @@ mod tests { message: "message2".to_string(), code: "code2".to_string(), hint: Some("hint2".to_string()), + quick_fixes: Vec::new(), }, range, }, diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index fe69048302..7b969b8abb 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -1691,6 +1691,7 @@ let c: number = "a"; rules: Default::default(), files: FilePatterns::new_with_base(temp_dir.path().to_path_buf()), reporter_kind: Default::default(), + fix: false, }; // test enabled diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 7d9c4318b5..de5f7e3576 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -2195,7 +2195,7 @@ impl Inner { })? } Some("deno-lint") => code_actions - .add_deno_lint_ignore_action( + .add_deno_lint_actions( &specifier, diagnostic, asset_or_doc.document().map(|d| d.text_info()), diff --git a/cli/tools/lint/mod.rs b/cli/tools/lint/mod.rs index 251efd9412..bf96eca06d 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, ParsedSource), AnyError> { + fix: bool, +) -> Result<(ParsedSource, Vec), 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), 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)>, 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 { + 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::>(); + 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, ParsedSource), AnyError> { +) -> Result<(ParsedSource, Vec), 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, ParsedSource), AnyError>, + result: Result<(ParsedSource, Vec), AnyError>, reporter_lock: Arc>>, ) -> 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) + } _ => (), } diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 09178cd46e..c5913e07b1 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -10197,6 +10197,104 @@ console.log(snake_case); client.shutdown(); } +#[test] +fn lsp_code_actions_lint_fixes() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "window;", + } + })); + let diagnostics = diagnostics.all(); + let diagnostic = &diagnostics[0]; + let res = client.write_request( + "textDocument/codeAction", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 6 } + }, + "context": { + "diagnostics": [diagnostic], + "only": ["quickfix"] + } + }), + ); + assert_eq!( + res, + json!([{ + "title": "Rename window to globalThis", + "kind": "quickfix", + "diagnostics": [diagnostic], + "edit": { + "changes": { + "file:///a/file.ts": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 6 } + }, + "newText": "globalThis" + }] + } + } + }, { + "title": "Disable no-window for this line", + "kind": "quickfix", + "diagnostics": [diagnostic], + "edit": { + "changes": { + "file:///a/file.ts": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 } + }, + "newText": "// deno-lint-ignore no-window\n" + }] + } + } + }, { + "title": "Disable no-window for the entire file", + "kind": "quickfix", + "diagnostics": [diagnostic], + "edit": { + "changes": { + "file:///a/file.ts": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 } + }, + "newText": "// deno-lint-ignore-file no-window\n" + }] + } + } + }, { + "title": "Ignore lint errors for the entire file", + "kind": "quickfix", + "diagnostics": [diagnostic], + "edit": { + "changes": { + "file:///a/file.ts": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 } + }, + "newText": "// deno-lint-ignore-file\n" + }] + } + } + }]) + ); + client.shutdown(); +} + #[test] fn lsp_lint_with_config() { let context = TestContextBuilder::new().use_temp_cwd().build(); diff --git a/tests/specs/lint/lint_fix/__test__.jsonc b/tests/specs/lint/lint_fix/__test__.jsonc new file mode 100644 index 0000000000..53736586f7 --- /dev/null +++ b/tests/specs/lint/lint_fix/__test__.jsonc @@ -0,0 +1,17 @@ +{ + "tempDir": true, + "steps": [{ + "args": "lint --rules-tags=recommended,jsr", + "output": "lint.out", + "exitCode": 1 + }, { + "args": "lint --fix --rules-tags=recommended,jsr", + "output": "lint_fixed.out" + }, { + "args": "lint --rules-tags=recommended,jsr", + "output": "lint_fixed.out" + }, { + "args": "run --allow-read --quiet http://localhost:4545/cat.ts a.ts", + "output": "a_fixed.out" + }] +} diff --git a/tests/specs/lint/lint_fix/a.ts b/tests/specs/lint/lint_fix/a.ts new file mode 100644 index 0000000000..6a1b87bc4b --- /dev/null +++ b/tests/specs/lint/lint_fix/a.ts @@ -0,0 +1,4 @@ +import { Type } from "./test.ts"; +export type MyType = Type; +console.log(window.value); +window.fetch; diff --git a/tests/specs/lint/lint_fix/a_fixed.out b/tests/specs/lint/lint_fix/a_fixed.out new file mode 100644 index 0000000000..5193be18ec --- /dev/null +++ b/tests/specs/lint/lint_fix/a_fixed.out @@ -0,0 +1,4 @@ +import type { Type } from "./test.ts"; +export type MyType = Type; +console.log(globalThis.value); +globalThis.fetch; diff --git a/tests/specs/lint/lint_fix/lint.out b/tests/specs/lint/lint_fix/lint.out new file mode 100644 index 0000000000..e292c28812 --- /dev/null +++ b/tests/specs/lint/lint_fix/lint.out @@ -0,0 +1,2 @@ +[WILDCARD]Found 4 problems (4 fixable via --fix) +Checked 1 file diff --git a/tests/specs/lint/lint_fix/lint_fixed.out b/tests/specs/lint/lint_fix/lint_fixed.out new file mode 100644 index 0000000000..c05ac45a1e --- /dev/null +++ b/tests/specs/lint/lint_fix/lint_fixed.out @@ -0,0 +1 @@ +Checked 1 file diff --git a/tests/testdata/cat.ts b/tests/testdata/cat.ts index 39745da89f..62c82ebca0 100644 --- a/tests/testdata/cat.ts +++ b/tests/testdata/cat.ts @@ -1,10 +1,4 @@ -import { copy } from "../../tests/util/std/streams/copy.ts"; -async function main() { - for (let i = 1; i < Deno.args.length; i++) { - const filename = Deno.args[i]; - const file = await Deno.open(filename); - await copy(file, Deno.stdout); - } -} +const filename = Deno.args[0]; +using file = await Deno.open(filename); -main(); +await file.readable.pipeTo(Deno.stdout.writable); diff --git a/tests/testdata/lint/expected_from_stdin.out b/tests/testdata/lint/expected_from_stdin.out index 59f32166fc..735b271f52 100644 --- a/tests/testdata/lint/expected_from_stdin.out +++ b/tests/testdata/lint/expected_from_stdin.out @@ -5,7 +5,7 @@ error[no-explicit-any]: `any` type is not allowed | ^^^ = hint: Use a specific type other than `any` - docs: https://lint.deno.land/#no-explicit-any + docs: https://lint.deno.land/rules/no-explicit-any Found 1 problem diff --git a/tests/testdata/lint/expected_quiet.out b/tests/testdata/lint/expected_quiet.out index e46a94a2d1..91c1a29cf1 100644 --- a/tests/testdata/lint/expected_quiet.out +++ b/tests/testdata/lint/expected_quiet.out @@ -5,7 +5,7 @@ error[ban-untagged-ignore]: Ignore directive requires lint rule name(s) | ^^^^^^^^^^^^^^^^^^^ = hint: Add one or more lint rule names. E.g. // deno-lint-ignore adjacent-overload-signatures - docs: https://lint.deno.land/#ban-untagged-ignore + docs: https://lint.deno.land/rules/ban-untagged-ignore error[no-empty]: Empty block statement @@ -15,6 +15,6 @@ error[no-empty]: Empty block statement | ^^ = hint: Add code or comment to the empty block - docs: https://lint.deno.land/#no-empty + docs: https://lint.deno.land/rules/no-empty diff --git a/tests/testdata/lint/with_config.out b/tests/testdata/lint/with_config.out index cffd6b9c73..f527bb7121 100644 --- a/tests/testdata/lint/with_config.out +++ b/tests/testdata/lint/with_config.out @@ -5,7 +5,7 @@ error[ban-untagged-todo]: TODO should be tagged with (@username) or (#issue) | ^^^^^^^^^^^^ = hint: Add a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123) - docs: https://lint.deno.land/#ban-untagged-todo + docs: https://lint.deno.land/rules/ban-untagged-todo error[no-unused-vars]: `add` is never used @@ -15,7 +15,7 @@ error[no-unused-vars]: `add` is never used | ^^^ = hint: If this is intentional, prefix it with an underscore like `_add` - docs: https://lint.deno.land/#no-unused-vars + docs: https://lint.deno.land/rules/no-unused-vars Found 2 problems diff --git a/tests/testdata/lint/with_config_and_flags.out b/tests/testdata/lint/with_config_and_flags.out index f3ad3cafb0..78e21ef8d8 100644 --- a/tests/testdata/lint/with_config_and_flags.out +++ b/tests/testdata/lint/with_config_and_flags.out @@ -5,7 +5,7 @@ error[ban-untagged-todo]: TODO should be tagged with (@username) or (#issue) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = hint: Add a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123) - docs: https://lint.deno.land/#ban-untagged-todo + docs: https://lint.deno.land/rules/ban-untagged-todo error[no-unused-vars]: `subtract` is never used @@ -15,7 +15,7 @@ error[no-unused-vars]: `subtract` is never used | ^^^^^^^^ = hint: If this is intentional, prefix it with an underscore like `_subtract` - docs: https://lint.deno.land/#no-unused-vars + docs: https://lint.deno.land/rules/no-unused-vars Found 2 problems diff --git a/tests/testdata/lint/with_config_without_tags.out b/tests/testdata/lint/with_config_without_tags.out index cffd6b9c73..f527bb7121 100644 --- a/tests/testdata/lint/with_config_without_tags.out +++ b/tests/testdata/lint/with_config_without_tags.out @@ -5,7 +5,7 @@ error[ban-untagged-todo]: TODO should be tagged with (@username) or (#issue) | ^^^^^^^^^^^^ = hint: Add a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123) - docs: https://lint.deno.land/#ban-untagged-todo + docs: https://lint.deno.land/rules/ban-untagged-todo error[no-unused-vars]: `add` is never used @@ -15,7 +15,7 @@ error[no-unused-vars]: `add` is never used | ^^^ = hint: If this is intentional, prefix it with an underscore like `_add` - docs: https://lint.deno.land/#no-unused-vars + docs: https://lint.deno.land/rules/no-unused-vars Found 2 problems