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)'
|
||||
test-group = 'serial'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'binary(e2e)'
|
||||
test-group = 'serial'
|
||||
|
||||
[profile.ci]
|
||||
# Print out output for failing tests as soon as they fail, and also at the end
|
||||
# of the run (for easy scrollability).
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ impl Fix {
|
|||
&self.edits
|
||||
}
|
||||
|
||||
pub fn into_edits(self) -> Vec<Edit> {
|
||||
self.edits
|
||||
}
|
||||
|
||||
/// Return the [`Applicability`] of the [`Fix`].
|
||||
pub fn applicability(&self) -> Applicability {
|
||||
self.applicability
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use crate::{completion, find_node::covering_node};
|
||||
|
||||
use ruff_db::{files::File, parsed::parsed_module};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_text_size::TextRange;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::create_suppression_fix;
|
||||
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
||||
|
||||
/// A `QuickFix` Code Action
|
||||
|
|
@ -18,26 +20,501 @@ pub fn code_actions(
|
|||
file: File,
|
||||
diagnostic_range: TextRange,
|
||||
diagnostic_id: &str,
|
||||
) -> Option<Vec<QuickFix>> {
|
||||
) -> Vec<QuickFix> {
|
||||
let registry = db.lint_registry();
|
||||
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 node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
||||
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()
|
||||
.map(|import| QuickFix {
|
||||
title: import.label,
|
||||
edits: vec![import.edit],
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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,
|
||||
};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use suppression::create_suppression_fix;
|
||||
pub use types::DisplaySettings;
|
||||
pub use types::ide_support::{
|
||||
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> {
|
||||
db: &'a dyn Db,
|
||||
file: File,
|
||||
|
|
|
|||
|
|
@ -82,9 +82,8 @@ impl BackgroundDocumentRequestHandler for CodeActionRequestHandler {
|
|||
let encoding = snapshot.encoding();
|
||||
if let Some(NumberOrString::String(diagnostic_id)) = &diagnostic.code
|
||||
&& 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 {
|
||||
title: action.title,
|
||||
kind: Some(CodeActionKind::QUICKFIX),
|
||||
|
|
|
|||
|
|
@ -51,5 +51,54 @@ expression: code_actions
|
|||
}
|
||||
},
|
||||
"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
|
||||
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
|
||||
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
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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 (keyword, body) = comment.split_once(':')?;
|
||||
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();
|
||||
|
||||
match keyword {
|
||||
|
|
|
|||
|
|
@ -573,16 +573,16 @@ impl Workspace {
|
|||
// 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,
|
||||
// which is dubious when you're in the middle of resolving symbols.
|
||||
if let Some(range) = diagnostic.inner.range()
|
||||
&& let Some(fixes) = ty_ide::code_actions(
|
||||
if let Some(range) = diagnostic.inner.range() {
|
||||
actions.extend(
|
||||
ty_ide::code_actions(
|
||||
&self.db,
|
||||
file_id.file,
|
||||
range,
|
||||
diagnostic.inner.id().as_str(),
|
||||
)
|
||||
{
|
||||
for action in fixes {
|
||||
actions.push(CodeAction {
|
||||
.into_iter()
|
||||
.map(|action| CodeAction {
|
||||
title: action.title,
|
||||
preferred: action.preferred,
|
||||
edits: action
|
||||
|
|
@ -590,8 +590,8 @@ impl Workspace {
|
|||
.into_iter()
|
||||
.map(|edit| edit_to_text_edit(self, file_id.file, &edit))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if actions.is_empty() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue