[ty] Add code action to ignore diagnostic on the current line (#21595)

This commit is contained in:
Micha Reiser 2025-11-29 15:41:54 +01:00 committed by GitHub
parent b2387f4eab
commit d40590c8f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 868 additions and 32 deletions

View File

@ -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).

View File

@ -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

View File

@ -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
}
} }
} }

View File

@ -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

View File

@ -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,

View File

@ -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,

View 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),

View File

@ -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
} }
] ]

View File

@ -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
}
]

View File

@ -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
}
]

View File

@ -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
} }
] ]

View File

@ -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
} }
] ]

View File

@ -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 {

View File

@ -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() {