Skip to content

Commit 9d5de3a

Browse files
committed
More tests, handle multiline strings, line continuation, and interpolations
1 parent 52f3c3e commit 9d5de3a

File tree

4 files changed

+364
-2
lines changed

4 files changed

+364
-2
lines changed

crates/ty_ide/src/code_action.rs

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,299 @@ mod tests {
9999
")
100100
}
101101

102+
#[test]
103+
fn add_ignore_existing_comment() {
104+
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 # fmt: off"#);
105+
106+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
107+
info[code-action]: Ignore 'unresolved-reference' for this line
108+
--> main.py:1:5
109+
|
110+
1 | b = a / 10 # fmt: off
111+
| ^
112+
|
113+
- b = a / 10 # fmt: off
114+
1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference]
115+
")
116+
}
117+
118+
#[test]
119+
fn add_ignore_trailing_whitespace() {
120+
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 "#);
121+
122+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
123+
info[code-action]: Ignore 'unresolved-reference' for this line
124+
--> main.py:1:5
125+
|
126+
1 | b = a / 10
127+
| ^
128+
|
129+
- b = a / 10
130+
1 + b = a / 10 # ty:ignore[unresolved-reference]
131+
")
132+
}
133+
134+
#[test]
135+
fn add_code_existing_ignore() {
136+
let test = CodeActionTest::with_source(
137+
r#"
138+
b = <START>a<END> / 0 # ty:ignore[division-by-zero]
139+
"#,
140+
);
141+
142+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
143+
info[code-action]: Ignore 'unresolved-reference' for this line
144+
--> main.py:2:17
145+
|
146+
2 | b = a / 0 # ty:ignore[division-by-zero]
147+
| ^
148+
|
149+
1 |
150+
- b = a / 0 # ty:ignore[division-by-zero]
151+
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
152+
3 |
153+
")
154+
}
155+
156+
#[test]
157+
fn add_code_existing_ignore_with_reason() {
158+
let test = CodeActionTest::with_source(
159+
r#"
160+
b = <START>a<END> / 0 # ty:ignore[division-by-zero] some explanation
161+
"#,
162+
);
163+
164+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
165+
info[code-action]: Ignore 'unresolved-reference' for this line
166+
--> main.py:2:17
167+
|
168+
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
169+
| ^
170+
|
171+
1 |
172+
- b = a / 0 # ty:ignore[division-by-zero] some explanation
173+
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
174+
3 |
175+
")
176+
}
177+
178+
#[test]
179+
fn add_code_existing_ignore_start_line() {
180+
let test = CodeActionTest::with_source(
181+
r#"
182+
b = (
183+
<START>a # ty:ignore[division-by-zero]
184+
/
185+
0<END>
186+
)
187+
"#,
188+
);
189+
190+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
191+
info[code-action]: Ignore 'unresolved-reference' for this line
192+
--> main.py:3:21
193+
|
194+
2 | b = (
195+
3 | / a # ty:ignore[division-by-zero]
196+
4 | | /
197+
5 | | 0
198+
| |_____________________^
199+
6 | )
200+
|
201+
1 |
202+
2 | b = (
203+
- a # ty:ignore[division-by-zero]
204+
3 + a # ty:ignore[division-by-zero, unresolved-reference]
205+
4 | /
206+
5 | 0
207+
6 | )
208+
")
209+
}
210+
211+
#[test]
212+
fn add_code_existing_ignore_end_line() {
213+
let test = CodeActionTest::with_source(
214+
r#"
215+
b = (
216+
<START>a
217+
/
218+
0<END> # ty:ignore[division-by-zero]
219+
)
220+
"#,
221+
);
222+
223+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
224+
info[code-action]: Ignore 'unresolved-reference' for this line
225+
--> main.py:3:21
226+
|
227+
2 | b = (
228+
3 | / a
229+
4 | | /
230+
5 | | 0 # ty:ignore[division-by-zero]
231+
| |_____________________^
232+
6 | )
233+
|
234+
2 | b = (
235+
3 | a
236+
4 | /
237+
- 0 # ty:ignore[division-by-zero]
238+
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
239+
6 | )
240+
7 |
241+
")
242+
}
243+
244+
#[test]
245+
fn add_code_existing_ignores() {
246+
let test = CodeActionTest::with_source(
247+
r#"
248+
b = (
249+
<START>a # ty:ignore[division-by-zero]
250+
/
251+
0<END> # ty:ignore[division-by-zero]
252+
)
253+
"#,
254+
);
255+
256+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
257+
info[code-action]: Ignore 'unresolved-reference' for this line
258+
--> main.py:3:21
259+
|
260+
2 | b = (
261+
3 | / a # ty:ignore[division-by-zero]
262+
4 | | /
263+
5 | | 0 # ty:ignore[division-by-zero]
264+
| |_____________________^
265+
6 | )
266+
|
267+
1 |
268+
2 | b = (
269+
- a # ty:ignore[division-by-zero]
270+
3 + a # ty:ignore[division-by-zero, unresolved-reference]
271+
4 | /
272+
5 | 0 # ty:ignore[division-by-zero]
273+
6 | )
274+
")
275+
}
276+
277+
#[test]
278+
fn add_code_interpolated_string() {
279+
let test = CodeActionTest::with_source(
280+
r#"
281+
b = f"""
282+
{<START>a<END>}
283+
more text
284+
"""
285+
"#,
286+
);
287+
288+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
289+
info[code-action]: Ignore 'unresolved-reference' for this line
290+
--> main.py:3:18
291+
|
292+
2 | b = f"""
293+
3 | {a}
294+
| ^
295+
4 | more text
296+
5 | """
297+
|
298+
2 | b = f"""
299+
3 | {a}
300+
4 | more text
301+
- """
302+
5 + """ # ty:ignore[unresolved-reference]
303+
6 |
304+
"#)
305+
}
306+
307+
#[test]
308+
fn add_code_multiline_interpolation() {
309+
let test = CodeActionTest::with_source(
310+
r#"
311+
b = f"""
312+
{
313+
<START>a<END>
314+
}
315+
more text
316+
"""
317+
"#,
318+
);
319+
320+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
321+
info[code-action]: Ignore 'unresolved-reference' for this line
322+
--> main.py:4:17
323+
|
324+
2 | b = f"""
325+
3 | {
326+
4 | a
327+
| ^
328+
5 | }
329+
6 | more text
330+
|
331+
1 |
332+
2 | b = f"""
333+
3 | {
334+
- a
335+
4 + a # ty:ignore[unresolved-reference]
336+
5 | }
337+
6 | more text
338+
7 | """
339+
"#)
340+
}
341+
342+
#[test]
343+
fn add_code_followed_by_multiline_string() {
344+
let test = CodeActionTest::with_source(
345+
r#"
346+
b = <START>a<END> + """
347+
more text
348+
"""
349+
"#,
350+
);
351+
352+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
353+
info[code-action]: Ignore 'unresolved-reference' for this line
354+
--> main.py:2:17
355+
|
356+
2 | b = a + """
357+
| ^
358+
3 | more text
359+
4 | """
360+
|
361+
1 |
362+
2 | b = a + """
363+
3 | more text
364+
- """
365+
4 + """ # ty:ignore[unresolved-reference]
366+
5 |
367+
"#)
368+
}
369+
370+
#[test]
371+
fn add_code_followed_by_continuation() {
372+
let test = CodeActionTest::with_source(
373+
r#"
374+
b = <START>a<END> \
375+
+ "test"
376+
"#,
377+
);
378+
379+
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
380+
info[code-action]: Ignore 'unresolved-reference' for this line
381+
--> main.py:2:17
382+
|
383+
2 | b = a \
384+
| ^
385+
3 | + "test"
386+
|
387+
1 |
388+
2 | b = a \
389+
- + "test"
390+
3 + + "test" # ty:ignore[unresolved-reference]
391+
4 |
392+
"#)
393+
}
394+
102395
pub(super) struct CodeActionTest {
103396
pub(super) db: ty_project::TestDb,
104397
pub(super) file: File,

crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,50 @@ a = test \
8585
+ 2 # type: ignore
8686
```
8787

88+
## Interpolated strings
89+
90+
```toml
91+
[environment]
92+
python-version = "3.14"
93+
```
94+
95+
Suppressions for expressions within interpolated strings can be placed after the interpolated string
96+
if it's a single-line interpolation.
97+
98+
```py
99+
a = f"""
100+
{test}
101+
""" # type: ignore
102+
```
103+
104+
For multiline-interpolation, put the ignore comment on the expression's start or end line:
105+
106+
```py
107+
a = f"""
108+
{
109+
10 / # type: ignore
110+
0
111+
}
112+
"""
113+
114+
a = f"""
115+
{
116+
10 /
117+
0 # type: ignore
118+
}
119+
"""
120+
```
121+
122+
But not at the end of the f-string:
123+
124+
```py
125+
a = f"""
126+
{
127+
10 / 0 # error: [division-by-zero]
128+
}
129+
""" # error: [unused-ignore-comment] # type: ignore
130+
```
131+
88132
## Codes
89133

90134
Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the

crates/ty_python_semantic/src/suppression.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use ruff_db::{files::File, parsed::parsed_module, source::source_text};
1717
use ruff_diagnostics::{Edit, Fix};
1818
use ruff_python_parser::TokenKind;
1919
use ruff_python_trivia::Cursor;
20-
use ruff_source_file::LineRanges;
2120
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
2221

2322
declare_lint! {
@@ -394,6 +393,7 @@ pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRa
394393
// If there's an existing `ty: ignore[]` comment, append the code to it instead of creating a new suppression comment.
395394
if let Some(existing) = existing_suppressions.next() {
396395
let comment_text = &source[existing.comment_range];
396+
// Only add to the existing ignore comment if it has no reason.
397397
if let Some(before_closing_paren) = comment_text.trim_end().strip_suffix(']') {
398398
let up_to_last_code = before_closing_paren.trim_end();
399399

@@ -414,7 +414,23 @@ pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRa
414414

415415
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
416416
// etc.
417-
let line_end = source.line_end(range.end());
417+
let parsed = parsed_module(db, file).load(db);
418+
let tokens_after = parsed.tokens().after(range.end());
419+
420+
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
421+
// in multiline-strings, inside f-strings, or after a line continuation because we can't
422+
// place a comment on those lines.
423+
let line_end = tokens_after
424+
.iter()
425+
.find(|token| {
426+
matches!(
427+
token.kind(),
428+
TokenKind::Newline | TokenKind::NonLogicalNewline
429+
)
430+
})
431+
.map(Ranged::start)
432+
.unwrap_or(source.text_len());
433+
418434
let up_to_line_end = &source[..line_end.to_usize()];
419435
let up_to_first_content = up_to_line_end.trim_end();
420436
let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len();

0 commit comments

Comments
 (0)