mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 15:49:44 -05:00
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.
This commit is contained in:
parent
2166aa8fb6
commit
ffbcad3800
20 changed files with 442 additions and 59 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -199,6 +199,7 @@ pub struct UninstallFlags {
|
|||
pub struct LintFlags {
|
||||
pub files: FileFlags,
|
||||
pub rules: bool,
|
||||
pub fix: bool,
|
||||
pub maybe_rules_tags: Option<Vec<String>>,
|
||||
pub maybe_rules_include: Option<Vec<String>>,
|
||||
pub maybe_rules_exclude: Option<Vec<String>>,
|
||||
|
@ -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::<String>("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,
|
||||
|
|
|
@ -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<LintFlags>,
|
||||
initial_cwd: &Path,
|
||||
) -> Result<Self, AnyError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Regex> =
|
|||
|
||||
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<DataQuickFixChange>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
quick_fixes: Vec<DataQuickFix>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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<SourceTextInfo>,
|
||||
maybe_parsed_source: Option<deno_ast::ParsedSource>,
|
||||
) -> Result<(), AnyError> {
|
||||
if let Some(data_quick_fixes) = diagnostic
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| serde_json::from_value::<Vec<DataQuickFix>>(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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
17
tests/specs/lint/lint_fix/__test__.jsonc
Normal file
17
tests/specs/lint/lint_fix/__test__.jsonc
Normal file
|
@ -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"
|
||||
}]
|
||||
}
|
4
tests/specs/lint/lint_fix/a.ts
Normal file
4
tests/specs/lint/lint_fix/a.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { Type } from "./test.ts";
|
||||
export type MyType = Type;
|
||||
console.log(window.value);
|
||||
window.fetch;
|
4
tests/specs/lint/lint_fix/a_fixed.out
Normal file
4
tests/specs/lint/lint_fix/a_fixed.out
Normal file
|
@ -0,0 +1,4 @@
|
|||
import type { Type } from "./test.ts";
|
||||
export type MyType = Type;
|
||||
console.log(globalThis.value);
|
||||
globalThis.fetch;
|
2
tests/specs/lint/lint_fix/lint.out
Normal file
2
tests/specs/lint/lint_fix/lint.out
Normal file
|
@ -0,0 +1,2 @@
|
|||
[WILDCARD]Found 4 problems (4 fixable via --fix)
|
||||
Checked 1 file
|
1
tests/specs/lint/lint_fix/lint_fixed.out
Normal file
1
tests/specs/lint/lint_fix/lint_fixed.out
Normal file
|
@ -0,0 +1 @@
|
|||
Checked 1 file
|
12
tests/testdata/cat.ts
vendored
12
tests/testdata/cat.ts
vendored
|
@ -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);
|
||||
|
|
2
tests/testdata/lint/expected_from_stdin.out
vendored
2
tests/testdata/lint/expected_from_stdin.out
vendored
|
@ -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
|
||||
|
|
4
tests/testdata/lint/expected_quiet.out
vendored
4
tests/testdata/lint/expected_quiet.out
vendored
|
@ -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
|
||||
|
||||
|
||||
|
|
4
tests/testdata/lint/with_config.out
vendored
4
tests/testdata/lint/with_config.out
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue