mirror of https://github.com/astral-sh/ruff
[ty] Add code action to ignore diagnostic on the current line (#21595)
This commit is contained in:
parent
b2387f4eab
commit
d40590c8f9
|
|
@ -7,10 +7,6 @@ serial = { max-threads = 1 }
|
||||||
filter = 'binary(file_watching)'
|
filter = 'binary(file_watching)'
|
||||||
test-group = 'serial'
|
test-group = 'serial'
|
||||||
|
|
||||||
[[profile.default.overrides]]
|
|
||||||
filter = 'binary(e2e)'
|
|
||||||
test-group = 'serial'
|
|
||||||
|
|
||||||
[profile.ci]
|
[profile.ci]
|
||||||
# Print out output for failing tests as soon as they fail, and also at the end
|
# Print out output for failing tests as soon as they fail, and also at the end
|
||||||
# of the run (for easy scrollability).
|
# of the run (for easy scrollability).
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,10 @@ impl Fix {
|
||||||
&self.edits
|
&self.edits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_edits(self) -> Vec<Edit> {
|
||||||
|
self.edits
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the [`Applicability`] of the [`Fix`].
|
/// Return the [`Applicability`] of the [`Fix`].
|
||||||
pub fn applicability(&self) -> Applicability {
|
pub fn applicability(&self) -> Applicability {
|
||||||
self.applicability
|
self.applicability
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::{completion, find_node::covering_node};
|
use crate::{completion, find_node::covering_node};
|
||||||
|
|
||||||
use ruff_db::{files::File, parsed::parsed_module};
|
use ruff_db::{files::File, parsed::parsed_module};
|
||||||
use ruff_diagnostics::Edit;
|
use ruff_diagnostics::Edit;
|
||||||
use ruff_text_size::TextRange;
|
use ruff_text_size::TextRange;
|
||||||
use ty_project::Db;
|
use ty_project::Db;
|
||||||
|
use ty_python_semantic::create_suppression_fix;
|
||||||
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
||||||
|
|
||||||
/// A `QuickFix` Code Action
|
/// A `QuickFix` Code Action
|
||||||
|
|
@ -18,26 +20,501 @@ pub fn code_actions(
|
||||||
file: File,
|
file: File,
|
||||||
diagnostic_range: TextRange,
|
diagnostic_range: TextRange,
|
||||||
diagnostic_id: &str,
|
diagnostic_id: &str,
|
||||||
) -> Option<Vec<QuickFix>> {
|
) -> Vec<QuickFix> {
|
||||||
let registry = db.lint_registry();
|
let registry = db.lint_registry();
|
||||||
let Ok(lint_id) = registry.get(diagnostic_id) else {
|
let Ok(lint_id) = registry.get(diagnostic_id) else {
|
||||||
return None;
|
return Vec::new();
|
||||||
};
|
};
|
||||||
if lint_id.name() == UNRESOLVED_REFERENCE.name() {
|
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
|
||||||
|
if lint_id.name() == UNRESOLVED_REFERENCE.name()
|
||||||
|
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range)
|
||||||
|
{
|
||||||
|
actions.extend(import_quick_fix);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push(QuickFix {
|
||||||
|
title: format!("Ignore '{}' for this line", lint_id.name()),
|
||||||
|
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),
|
||||||
|
preferred: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_import_symbol_quick_fix(
|
||||||
|
db: &dyn Db,
|
||||||
|
file: File,
|
||||||
|
diagnostic_range: TextRange,
|
||||||
|
) -> Option<impl Iterator<Item = QuickFix>> {
|
||||||
let parsed = parsed_module(db, file).load(db);
|
let parsed = parsed_module(db, file).load(db);
|
||||||
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
||||||
let symbol = &node.expr_name()?.id;
|
let symbol = &node.expr_name()?.id;
|
||||||
|
|
||||||
let fixes = completion::missing_imports(db, file, &parsed, symbol, node)
|
Some(
|
||||||
|
completion::missing_imports(db, file, &parsed, symbol, node)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|import| QuickFix {
|
.map(|import| QuickFix {
|
||||||
title: import.label,
|
title: import.label,
|
||||||
edits: vec![import.edit],
|
edits: vec![import.edit],
|
||||||
preferred: true,
|
preferred: true,
|
||||||
})
|
}),
|
||||||
.collect();
|
)
|
||||||
Some(fixes)
|
}
|
||||||
} else {
|
|
||||||
None
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use crate::code_actions;
|
||||||
|
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ruff_db::{
|
||||||
|
diagnostic::{
|
||||||
|
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
|
||||||
|
LintName, Span, SubDiagnostic,
|
||||||
|
},
|
||||||
|
files::{File, system_path_to_file},
|
||||||
|
system::{DbWithWritableSystem, SystemPathBuf},
|
||||||
|
};
|
||||||
|
use ruff_diagnostics::Fix;
|
||||||
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
|
use ty_project::ProjectMetadata;
|
||||||
|
use ty_python_semantic::{lint::LintMetadata, types::UNRESOLVED_REFERENCE};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_ignore() {
|
||||||
|
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10"#);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:1:5
|
||||||
|
|
|
||||||
|
1 | b = a / 10
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
- b = a / 10
|
||||||
|
1 + b = a / 10 # ty:ignore[unresolved-reference]
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_ignore_existing_comment() {
|
||||||
|
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 # fmt: off"#);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:1:5
|
||||||
|
|
|
||||||
|
1 | b = a / 10 # fmt: off
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
- b = a / 10 # fmt: off
|
||||||
|
1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference]
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_ignore_trailing_whitespace() {
|
||||||
|
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 "#);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:1:5
|
||||||
|
|
|
||||||
|
1 | b = a / 10
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
- b = a / 10
|
||||||
|
1 + b = a / 10 # ty:ignore[unresolved-reference]
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> / 0 # ty:ignore[division-by-zero]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a / 0 # ty:ignore[division-by-zero]
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
- b = a / 0 # ty:ignore[division-by-zero]
|
||||||
|
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||||
|
3 |
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore_trailing_comma() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> / 0 # ty:ignore[division-by-zero,]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a / 0 # ty:ignore[division-by-zero,]
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
- b = a / 0 # ty:ignore[division-by-zero,]
|
||||||
|
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||||
|
3 |
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore_trailing_whitespace() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> / 0 # ty:ignore[division-by-zero ]
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a / 0 # ty:ignore[division-by-zero ]
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
- b = a / 0 # ty:ignore[division-by-zero ]
|
||||||
|
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ]
|
||||||
|
3 |
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore_with_reason() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> / 0 # ty:ignore[division-by-zero] some explanation
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||||
|
| ^
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
- b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||||
|
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
|
||||||
|
3 |
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore_start_line() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = (
|
||||||
|
<START>a # ty:ignore[division-by-zero]
|
||||||
|
/
|
||||||
|
0<END>
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:3:21
|
||||||
|
|
|
||||||
|
2 | b = (
|
||||||
|
3 | / a # ty:ignore[division-by-zero]
|
||||||
|
4 | | /
|
||||||
|
5 | | 0
|
||||||
|
| |_____________________^
|
||||||
|
6 | )
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | b = (
|
||||||
|
- a # ty:ignore[division-by-zero]
|
||||||
|
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||||
|
4 | /
|
||||||
|
5 | 0
|
||||||
|
6 | )
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignore_end_line() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = (
|
||||||
|
<START>a
|
||||||
|
/
|
||||||
|
0<END> # ty:ignore[division-by-zero]
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:3:21
|
||||||
|
|
|
||||||
|
2 | b = (
|
||||||
|
3 | / a
|
||||||
|
4 | | /
|
||||||
|
5 | | 0 # ty:ignore[division-by-zero]
|
||||||
|
| |_____________________^
|
||||||
|
6 | )
|
||||||
|
|
|
||||||
|
2 | b = (
|
||||||
|
3 | a
|
||||||
|
4 | /
|
||||||
|
- 0 # ty:ignore[division-by-zero]
|
||||||
|
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||||
|
6 | )
|
||||||
|
7 |
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_existing_ignores() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = (
|
||||||
|
<START>a # ty:ignore[division-by-zero]
|
||||||
|
/
|
||||||
|
0<END> # ty:ignore[division-by-zero]
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:3:21
|
||||||
|
|
|
||||||
|
2 | b = (
|
||||||
|
3 | / a # ty:ignore[division-by-zero]
|
||||||
|
4 | | /
|
||||||
|
5 | | 0 # ty:ignore[division-by-zero]
|
||||||
|
| |_____________________^
|
||||||
|
6 | )
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | b = (
|
||||||
|
- a # ty:ignore[division-by-zero]
|
||||||
|
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||||
|
4 | /
|
||||||
|
5 | 0 # ty:ignore[division-by-zero]
|
||||||
|
6 | )
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_interpolated_string() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = f"""
|
||||||
|
{<START>a<END>}
|
||||||
|
more text
|
||||||
|
"""
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:3:18
|
||||||
|
|
|
||||||
|
2 | b = f"""
|
||||||
|
3 | {a}
|
||||||
|
| ^
|
||||||
|
4 | more text
|
||||||
|
5 | """
|
||||||
|
|
|
||||||
|
2 | b = f"""
|
||||||
|
3 | {a}
|
||||||
|
4 | more text
|
||||||
|
- """
|
||||||
|
5 + """ # ty:ignore[unresolved-reference]
|
||||||
|
6 |
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_multiline_interpolation() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = f"""
|
||||||
|
{
|
||||||
|
<START>a<END>
|
||||||
|
}
|
||||||
|
more text
|
||||||
|
"""
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:4:17
|
||||||
|
|
|
||||||
|
2 | b = f"""
|
||||||
|
3 | {
|
||||||
|
4 | a
|
||||||
|
| ^
|
||||||
|
5 | }
|
||||||
|
6 | more text
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | b = f"""
|
||||||
|
3 | {
|
||||||
|
- a
|
||||||
|
4 + a # ty:ignore[unresolved-reference]
|
||||||
|
5 | }
|
||||||
|
6 | more text
|
||||||
|
7 | """
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_followed_by_multiline_string() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> + """
|
||||||
|
more text
|
||||||
|
"""
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a + """
|
||||||
|
| ^
|
||||||
|
3 | more text
|
||||||
|
4 | """
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | b = a + """
|
||||||
|
3 | more text
|
||||||
|
- """
|
||||||
|
4 + """ # ty:ignore[unresolved-reference]
|
||||||
|
5 |
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_code_followed_by_continuation() {
|
||||||
|
let test = CodeActionTest::with_source(
|
||||||
|
r#"
|
||||||
|
b = <START>a<END> \
|
||||||
|
+ "test"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||||
|
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||||
|
--> main.py:2:17
|
||||||
|
|
|
||||||
|
2 | b = a \
|
||||||
|
| ^
|
||||||
|
3 | + "test"
|
||||||
|
|
|
||||||
|
1 |
|
||||||
|
2 | b = a \
|
||||||
|
- + "test"
|
||||||
|
3 + + "test" # ty:ignore[unresolved-reference]
|
||||||
|
4 |
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct CodeActionTest {
|
||||||
|
pub(super) db: ty_project::TestDb,
|
||||||
|
pub(super) file: File,
|
||||||
|
pub(super) diagnostic_range: TextRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionTest {
|
||||||
|
pub(super) fn with_source(source: &str) -> Self {
|
||||||
|
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||||
|
"test".into(),
|
||||||
|
SystemPathBuf::from("/"),
|
||||||
|
));
|
||||||
|
|
||||||
|
db.init_program().unwrap();
|
||||||
|
|
||||||
|
let mut cleansed = source.to_string();
|
||||||
|
|
||||||
|
let start = cleansed
|
||||||
|
.find("<START>")
|
||||||
|
.expect("source text should contain a `<START>` marker");
|
||||||
|
cleansed.replace_range(start..start + "<START>".len(), "");
|
||||||
|
|
||||||
|
let end = cleansed
|
||||||
|
.find("<END>")
|
||||||
|
.expect("source text should contain a `<END>` marker");
|
||||||
|
|
||||||
|
cleansed.replace_range(end..end + "<END>".len(), "");
|
||||||
|
|
||||||
|
assert!(start <= end, "<START> marker should be before <END> marker");
|
||||||
|
|
||||||
|
db.write_file("main.py", cleansed)
|
||||||
|
.expect("write to memory file system to be successful");
|
||||||
|
|
||||||
|
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
db,
|
||||||
|
file,
|
||||||
|
diagnostic_range: TextRange::new(
|
||||||
|
TextSize::try_from(start).unwrap(),
|
||||||
|
TextSize::try_from(end).unwrap(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn code_actions(&self, lint: &'static LintMetadata) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
let config = DisplayDiagnosticConfig::default()
|
||||||
|
.color(false)
|
||||||
|
.show_fix_diff(true)
|
||||||
|
.format(DiagnosticFormat::Full);
|
||||||
|
|
||||||
|
for mut action in code_actions(&self.db, self.file, self.diagnostic_range, &lint.name) {
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
DiagnosticId::Lint(LintName::of("code-action")),
|
||||||
|
ruff_db::diagnostic::Severity::Info,
|
||||||
|
action.title,
|
||||||
|
);
|
||||||
|
|
||||||
|
diagnostic.annotate(Annotation::primary(
|
||||||
|
Span::from(self.file).with_range(self.diagnostic_range),
|
||||||
|
));
|
||||||
|
|
||||||
|
if action.preferred {
|
||||||
|
diagnostic.sub(SubDiagnostic::new(
|
||||||
|
ruff_db::diagnostic::SubDiagnosticSeverity::Help,
|
||||||
|
"This is a preferred code action",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let first_edit = action.edits.remove(0);
|
||||||
|
diagnostic.set_fix(Fix::safe_edits(first_edit, action.edits));
|
||||||
|
|
||||||
|
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,50 @@ a = test \
|
||||||
+ 2 # type: ignore
|
+ 2 # type: ignore
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Interpolated strings
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.14"
|
||||||
|
```
|
||||||
|
|
||||||
|
Suppressions for expressions within interpolated strings can be placed after the interpolated string
|
||||||
|
if it's a single-line interpolation.
|
||||||
|
|
||||||
|
```py
|
||||||
|
a = f"""
|
||||||
|
{test}
|
||||||
|
""" # type: ignore
|
||||||
|
```
|
||||||
|
|
||||||
|
For multiline-interpolation, put the ignore comment on the expression's start or end line:
|
||||||
|
|
||||||
|
```py
|
||||||
|
a = f"""
|
||||||
|
{
|
||||||
|
10 / # type: ignore
|
||||||
|
0
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
a = f"""
|
||||||
|
{
|
||||||
|
10 /
|
||||||
|
0 # type: ignore
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
But not at the end of the f-string:
|
||||||
|
|
||||||
|
```py
|
||||||
|
a = f"""
|
||||||
|
{
|
||||||
|
10 / 0 # error: [division-by-zero]
|
||||||
|
}
|
||||||
|
""" # error: [unused-ignore-comment] # type: ignore
|
||||||
|
```
|
||||||
|
|
||||||
## Codes
|
## Codes
|
||||||
|
|
||||||
Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the
|
Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ pub use semantic_model::{
|
||||||
Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel,
|
Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel,
|
||||||
};
|
};
|
||||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||||
|
pub use suppression::create_suppression_fix;
|
||||||
pub use types::DisplaySettings;
|
pub use types::DisplaySettings;
|
||||||
pub use types::ide_support::{
|
pub use types::ide_support::{
|
||||||
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op,
|
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op,
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,77 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a fix for adding a suppression comment to suppress `lint` for `range`.
|
||||||
|
///
|
||||||
|
/// The fix prefers adding the code to an existing `ty: ignore[]` comment over
|
||||||
|
/// adding a new suppression comment.
|
||||||
|
pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix {
|
||||||
|
let suppressions = suppressions(db, file);
|
||||||
|
let source = source_text(db, file);
|
||||||
|
|
||||||
|
let mut existing_suppressions = suppressions.line_suppressions(range).filter(|suppression| {
|
||||||
|
matches!(
|
||||||
|
suppression.target,
|
||||||
|
SuppressionTarget::Lint(_) | SuppressionTarget::Empty,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's an existing `ty: ignore[]` comment, append the code to it instead of creating a new suppression comment.
|
||||||
|
if let Some(existing) = existing_suppressions.next() {
|
||||||
|
let comment_text = &source[existing.comment_range];
|
||||||
|
// Only add to the existing ignore comment if it has no reason.
|
||||||
|
if let Some(before_closing_paren) = comment_text.trim_end().strip_suffix(']') {
|
||||||
|
let up_to_last_code = before_closing_paren.trim_end();
|
||||||
|
|
||||||
|
let insertion = if up_to_last_code.ends_with(',') {
|
||||||
|
format!(" {id}", id = id.name())
|
||||||
|
} else {
|
||||||
|
format!(", {id}", id = id.name())
|
||||||
|
};
|
||||||
|
|
||||||
|
let relative_offset_from_end = comment_text.text_len() - up_to_last_code.text_len();
|
||||||
|
|
||||||
|
return Fix::safe_edit(Edit::insertion(
|
||||||
|
insertion,
|
||||||
|
existing.comment_range.end() - relative_offset_from_end,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
|
||||||
|
// etc.
|
||||||
|
let parsed = parsed_module(db, file).load(db);
|
||||||
|
let tokens_after = parsed.tokens().after(range.end());
|
||||||
|
|
||||||
|
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
|
||||||
|
// in multiline-strings, inside f-strings, or after a line continuation because we can't
|
||||||
|
// place a comment on those lines.
|
||||||
|
let line_end = tokens_after
|
||||||
|
.iter()
|
||||||
|
.find(|token| {
|
||||||
|
matches!(
|
||||||
|
token.kind(),
|
||||||
|
TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(Ranged::start)
|
||||||
|
.unwrap_or(source.text_len());
|
||||||
|
|
||||||
|
let up_to_line_end = &source[..line_end.to_usize()];
|
||||||
|
let up_to_first_content = up_to_line_end.trim_end();
|
||||||
|
let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len();
|
||||||
|
|
||||||
|
let insertion = format!(" # ty:ignore[{id}]", id = id.name());
|
||||||
|
|
||||||
|
Fix::safe_edit(if trailing_whitespace_len == TextSize::ZERO {
|
||||||
|
Edit::insertion(insertion, line_end)
|
||||||
|
} else {
|
||||||
|
// `expr # fmt: off<trailing_whitespace>`
|
||||||
|
// Trim the trailing whitespace
|
||||||
|
Edit::replacement(insertion, line_end - trailing_whitespace_len, line_end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
struct CheckSuppressionsContext<'a> {
|
struct CheckSuppressionsContext<'a> {
|
||||||
db: &'a dyn Db,
|
db: &'a dyn Db,
|
||||||
file: File,
|
file: File,
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,8 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler {
|
||||||
let encoding = snapshot.encoding();
|
let encoding = snapshot.encoding();
|
||||||
if let Some(NumberOrString::String(diagnostic_id)) = &diagnostic.code
|
if let Some(NumberOrString::String(diagnostic_id)) = &diagnostic.code
|
||||||
&& let Some(range) = diagnostic.range.to_text_range(db, file, url, encoding)
|
&& let Some(range) = diagnostic.range.to_text_range(db, file, url, encoding)
|
||||||
&& let Some(fixes) = code_actions(db, file, range, diagnostic_id)
|
|
||||||
{
|
{
|
||||||
for action in fixes {
|
for action in code_actions(db, file, range, diagnostic_id) {
|
||||||
actions.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
|
actions.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
|
||||||
title: action.title,
|
title: action.title,
|
||||||
kind: Some(CodeActionKind::QUICKFIX),
|
kind: Some(CodeActionKind::QUICKFIX),
|
||||||
|
|
|
||||||
|
|
@ -51,5 +51,54 @@ expression: code_actions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isPreferred": true
|
"isPreferred": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ignore 'unused-ignore-comment' for this line",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 12
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 42
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 2,
|
||||||
|
"code": "unused-ignore-comment",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#unused-ignore-comment"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Unused `ty: ignore` directive",
|
||||||
|
"relatedInformation": [],
|
||||||
|
"tags": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 41
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": ", unused-ignore-comment"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,51 @@
|
||||||
source: crates/ty_server/tests/e2e/code_actions.rs
|
source: crates/ty_server/tests/e2e/code_actions.rs
|
||||||
expression: code_actions
|
expression: code_actions
|
||||||
---
|
---
|
||||||
null
|
[
|
||||||
|
{
|
||||||
|
"title": "Ignore 'unresolved-reference' for this line",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 3
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 1,
|
||||||
|
"code": "unresolved-reference",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#unresolved-reference"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Name `typing` used when not defined",
|
||||||
|
"relatedInformation": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 24
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": " # ty:ignore[unresolved-reference]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,51 @@
|
||||||
source: crates/ty_server/tests/e2e/code_actions.rs
|
source: crates/ty_server/tests/e2e/code_actions.rs
|
||||||
expression: code_actions
|
expression: code_actions
|
||||||
---
|
---
|
||||||
null
|
[
|
||||||
|
{
|
||||||
|
"title": "Ignore 'possibly-missing-attribute' for this line",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 2,
|
||||||
|
"code": "possibly-missing-attribute",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#possibly-missing-attribute"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Submodule `parser` may not be available as an attribute on module `html`",
|
||||||
|
"relatedInformation": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 11
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": " # ty:ignore[possibly-missing-attribute]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -94,5 +94,51 @@ expression: code_actions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isPreferred": true
|
"isPreferred": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ignore 'unresolved-reference' for this line",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 1
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 1,
|
||||||
|
"code": "unresolved-reference",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#unresolved-reference"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Name `deprecated` used when not defined",
|
||||||
|
"relatedInformation": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 28
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 28
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": " # ty:ignore[unresolved-reference]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -94,5 +94,51 @@ expression: code_actions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isPreferred": true
|
"isPreferred": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ignore 'unresolved-reference' for this line",
|
||||||
|
"kind": "quickfix",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 3
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 1,
|
||||||
|
"code": "unresolved-reference",
|
||||||
|
"codeDescription": {
|
||||||
|
"href": "https://ty.dev/rules#unresolved-reference"
|
||||||
|
},
|
||||||
|
"source": "ty",
|
||||||
|
"message": "Name `Literal` used when not defined",
|
||||||
|
"relatedInformation": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edit": {
|
||||||
|
"changes": {
|
||||||
|
"file://<temp_dir>/src/foo.py": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 17
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 0,
|
||||||
|
"character": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newText": " # ty:ignore[unresolved-reference]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isPreferred": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,15 @@ impl<'a> UnparsedAssertion<'a> {
|
||||||
let comment = comment.trim().strip_prefix('#')?.trim();
|
let comment = comment.trim().strip_prefix('#')?.trim();
|
||||||
let (keyword, body) = comment.split_once(':')?;
|
let (keyword, body) = comment.split_once(':')?;
|
||||||
let keyword = keyword.trim();
|
let keyword = keyword.trim();
|
||||||
|
|
||||||
|
// Support other pragma comments coming after `error` or `revealed`, e.g.
|
||||||
|
// `# error: [code] # type: ignore` (nested pragma comments)
|
||||||
|
let body = if let Some((before_nested, _)) = body.split_once('#') {
|
||||||
|
before_nested
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
};
|
||||||
|
|
||||||
let body = body.trim();
|
let body = body.trim();
|
||||||
|
|
||||||
match keyword {
|
match keyword {
|
||||||
|
|
|
||||||
|
|
@ -573,16 +573,16 @@ impl Workspace {
|
||||||
// This is only for actions that are messy to compute at the time of the diagnostic.
|
// This is only for actions that are messy to compute at the time of the diagnostic.
|
||||||
// For instance, suggesting imports requires finding symbols for the entire project,
|
// For instance, suggesting imports requires finding symbols for the entire project,
|
||||||
// which is dubious when you're in the middle of resolving symbols.
|
// which is dubious when you're in the middle of resolving symbols.
|
||||||
if let Some(range) = diagnostic.inner.range()
|
if let Some(range) = diagnostic.inner.range() {
|
||||||
&& let Some(fixes) = ty_ide::code_actions(
|
actions.extend(
|
||||||
|
ty_ide::code_actions(
|
||||||
&self.db,
|
&self.db,
|
||||||
file_id.file,
|
file_id.file,
|
||||||
range,
|
range,
|
||||||
diagnostic.inner.id().as_str(),
|
diagnostic.inner.id().as_str(),
|
||||||
)
|
)
|
||||||
{
|
.into_iter()
|
||||||
for action in fixes {
|
.map(|action| CodeAction {
|
||||||
actions.push(CodeAction {
|
|
||||||
title: action.title,
|
title: action.title,
|
||||||
preferred: action.preferred,
|
preferred: action.preferred,
|
||||||
edits: action
|
edits: action
|
||||||
|
|
@ -590,8 +590,8 @@ impl Workspace {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|edit| edit_to_text_edit(self, file_id.file, &edit))
|
.map(|edit| edit_to_text_edit(self, file_id.file, &edit))
|
||||||
.collect(),
|
.collect(),
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if actions.is_empty() {
|
if actions.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue