Skip to content

Commit 25fd417

Browse files
committed
Initial type: ignore support
1 parent f0012df commit 25fd417

File tree

5 files changed

+197
-29
lines changed

5 files changed

+197
-29
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Suppressing errors with `type: ignore`
2+
3+
Type check errors can be suppressed by a `type: ignore` comment on the same line as the violation.
4+
5+
## Simple `type: ignore`
6+
7+
```py
8+
a = 4 + test # type: ignore
9+
```
10+
11+
## In parenthesized expression
12+
13+
```py
14+
a = (
15+
4 + test # type: ignore
16+
) # fmt: skip
17+
```
18+
19+
## Before opening parentheses
20+
21+
A suppression that applies to all errors before the openign parentheses.
22+
23+
```py
24+
a: Test = ( # type: ignore
25+
5
26+
) # fmt: skip
27+
```
28+
29+
## Multiline string
30+
31+
```py
32+
a: int = 4
33+
a = """
34+
This is a multiline string and the suppression is at its end
35+
""" # type: ignore
36+
```
37+
38+
## Line continuations
39+
40+
Suppressions after a line continuation apply to all previous lines.
41+
42+
```py
43+
# fmt: off
44+
a = test \
45+
+ 2 # type: ignore
46+
47+
a = test \
48+
+ a \
49+
+ 2 # type: ignore
50+
```
51+
52+
## Nested comments
53+
54+
TODO: We should support this for better interopability with other suppression comments.
55+
56+
```py
57+
# fmt: off
58+
# error: [unresolved-reference]
59+
a = test \
60+
+ 2 # fmt: skip # type: ignore
61+
62+
a = test \
63+
+ 2 # type: ignore # fmt: skip
64+
```
65+
66+
## Misspelled `type: ignore`
67+
68+
```py
69+
# error: [unresolved-reference]
70+
a = test + 2 # type: ignoree
71+
```
72+
73+
## Invalid - ignore on opening parentheses
74+
75+
`type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses
76+
in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot.
77+
78+
```py
79+
# fmt: off
80+
a = ( # type: ignore
81+
test + 4 # error: [unresolved-reference]
82+
)
83+
```

crates/red_knot_python_semantic/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod semantic_index;
2222
mod semantic_model;
2323
pub(crate) mod site_packages;
2424
mod stdlib;
25+
mod suppression;
2526
pub(crate) mod symbol;
2627
pub mod types;
2728
mod unpack;
Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,100 @@
1-
use salsa;
1+
use std::cmp::Ordering;
22

3-
use ruff_db::{files::File, parsed::comment_ranges, source::source_text};
3+
use ruff_python_parser::TokenKind;
4+
use ruff_source_file::LineRanges;
5+
use ruff_text_size::{Ranged, TextRange, TextSize};
6+
7+
use ruff_db::{files::File, parsed::parsed_module, source::source_text};
48
use ruff_index::{newtype_index, IndexVec};
59

610
use crate::{lint::LintId, Db};
711

812
#[salsa::tracked(return_ref)]
9-
pub(crate) fn suppressions(db: &dyn Db, file: File) -> IndexVec<SuppressionIndex, Suppression> {
10-
let comments = comment_ranges(db.upcast(), file);
13+
pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
1114
let source = source_text(db.upcast(), file);
15+
let parsed = parsed_module(db.upcast(), file);
1216

1317
let mut suppressions = IndexVec::default();
18+
let mut line_start = source.bom_start_offset();
19+
20+
for token in parsed.tokens() {
21+
match token.kind() {
22+
TokenKind::Comment => {
23+
let text = &source[token.range()];
24+
25+
let suppressed_range = TextRange::new(line_start, token.end());
1426

15-
for range in comments {
16-
let text = &source[range];
17-
18-
if text.starts_with("# type: ignore") {
19-
suppressions.push(Suppression {
20-
target: None,
21-
kind: SuppressionKind::TypeIgnore,
22-
});
23-
} else if text.starts_with("# knot: ignore") {
24-
suppressions.push(Suppression {
25-
target: None,
26-
kind: SuppressionKind::KnotIgnore,
27-
});
27+
if text.strip_prefix("# type: ignore").is_some_and(|suffix| {
28+
suffix.is_empty() || suffix.starts_with(char::is_whitespace)
29+
}) {
30+
suppressions.push(Suppression { suppressed_range });
31+
}
32+
}
33+
TokenKind::Newline | TokenKind::NonLogicalNewline => {
34+
line_start = token.range().end();
35+
}
36+
_ => {}
2837
}
2938
}
3039

31-
suppressions
40+
Suppressions { suppressions }
41+
}
42+
43+
/// The suppression comments of a single file.
44+
#[derive(Clone, Debug, Eq, PartialEq)]
45+
pub(crate) struct Suppressions {
46+
/// The suppressions sorted by the suppressed range.
47+
suppressions: IndexVec<SuppressionIndex, Suppression>,
48+
}
49+
50+
impl Suppressions {
51+
pub(crate) fn find_suppression(
52+
&self,
53+
range: TextRange,
54+
_id: LintId,
55+
) -> Option<SuppressionIndex> {
56+
let enclosing_index = self.enclosing_suppression(range.end())?;
57+
58+
// TODO(micha):
59+
// * Test if the suppression suppresses the passed lint
60+
61+
Some(enclosing_index)
62+
}
63+
64+
fn enclosing_suppression(&self, offset: TextSize) -> Option<SuppressionIndex> {
65+
self.suppressions
66+
.binary_search_by(|suppression| {
67+
if suppression.suppressed_range.contains(offset) {
68+
Ordering::Equal
69+
} else if suppression.suppressed_range.end() < offset {
70+
Ordering::Less
71+
} else {
72+
Ordering::Greater
73+
}
74+
})
75+
.ok()
76+
}
77+
}
78+
79+
impl std::ops::Index<SuppressionIndex> for Suppressions {
80+
type Output = Suppression;
81+
82+
fn index(&self, index: SuppressionIndex) -> &Self::Output {
83+
&self.suppressions[index]
84+
}
3285
}
3386

3487
#[newtype_index]
3588
pub(crate) struct SuppressionIndex;
3689

90+
/// A `type: ignore` or `knot: ignore` suppression comment.
3791
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
3892
pub(crate) struct Suppression {
39-
target: Option<LintId>,
40-
kind: SuppressionKind,
41-
}
42-
43-
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
44-
pub(crate) enum SuppressionKind {
45-
/// A `type: ignore` comment
46-
TypeIgnore,
47-
48-
/// A `knot: ignore` comment
49-
KnotIgnore,
93+
/// The range for which this suppression applies.
94+
/// Most of the time, this is the range of the comment's line.
95+
/// However, there are few cases where the range gets expanted to
96+
/// cover multiple lines:
97+
/// * multiline strings: `expr + """multiline\nstring""" # type: ignore`
98+
/// * line continuations: `expr \ + "test" # type: ignore`
99+
suppressed_range: TextRange,
50100
}

crates/red_knot_python_semantic/src/types/context.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use ruff_text_size::Ranged;
1010

1111
use crate::{
1212
lint::{LintId, LintMetadata},
13+
suppression::suppressions,
1314
Db,
1415
};
1516

@@ -74,6 +75,15 @@ impl<'db> InferContext<'db> {
7475
return;
7576
};
7677

78+
let suppressions = suppressions(self.db, self.file);
79+
80+
if suppressions
81+
.find_suppression(node.range(), LintId::of(lint))
82+
.is_some()
83+
{
84+
return;
85+
}
86+
7787
self.report_diagnostic(node, DiagnosticId::Lint(lint.name()), severity, message);
7888
}
7989

crates/ruff_index/src/slice.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::vec::IndexVec;
22
use crate::Idx;
3+
use std::cmp::Ordering;
34
use std::fmt::{Debug, Formatter};
45
use std::marker::PhantomData;
56
use std::ops::{Index, IndexMut, Range};
@@ -117,6 +118,29 @@ impl<I: Idx, T> IndexSlice<I, T> {
117118
Err(i) => Err(Idx::new(i)),
118119
}
119120
}
121+
122+
#[inline]
123+
pub fn binary_search_by<'a, F>(&'a self, f: F) -> Result<I, I>
124+
where
125+
F: FnMut(&'a T) -> Ordering,
126+
{
127+
match self.raw.binary_search_by(f) {
128+
Ok(i) => Ok(Idx::new(i)),
129+
Err(i) => Err(Idx::new(i)),
130+
}
131+
}
132+
133+
#[inline]
134+
pub fn binary_search_by_key<'a, B, F>(&'a self, key: &B, f: F) -> Result<I, I>
135+
where
136+
F: FnMut(&'a T) -> B,
137+
B: Ord,
138+
{
139+
match self.raw.binary_search_by_key(key, f) {
140+
Ok(i) => Ok(Idx::new(i)),
141+
Err(i) => Err(Idx::new(i)),
142+
}
143+
}
120144
}
121145

122146
impl<I, T> Debug for IndexSlice<I, T>

0 commit comments

Comments
 (0)