Skip to content

Commit 3e58f1f

Browse files
committed
Initial type: ignore support
1 parent f0012df commit 3e58f1f

7 files changed

Lines changed: 209 additions & 27 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
)
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+
)
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+
a = test \
44+
+ 2 # type: ignore
45+
46+
a = test \
47+
+ a \
48+
+ 2 # type: ignore
49+
```
50+
51+
## Nested comments
52+
53+
TODO: We should support this for better interopability with other suppression comments.
54+
55+
```py
56+
# error: [unresolved-reference]
57+
a = test \
58+
+ 2 # fmt: skip # type: ignore
59+
60+
a = test \
61+
+ 2 # type: ignore # fmt: skip
62+
```
63+
64+
## Misspelled `type: ignore`
65+
```py
66+
# error: [unresolved-reference]
67+
a = test + 2 # type: ignoree
68+
```
69+
70+
## Invalid - ignore on opening parentheses
71+
72+
`type: ignore` comments after an opening parentheses suppress any
73+
type errors inside the parentheses in Pyright. Neither Ruff, nor
74+
mypy support this and neither does Red Knot.
75+
76+
```py
77+
a = ( # type: ignore
78+
test + 4 # error: [unresolved-reference]
79+
)
80+
```

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: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,93 @@
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 {
31+
target: None,
32+
comment_range: token.range(),
33+
suppressed_range,
34+
});
35+
}
36+
}
37+
TokenKind::Newline | TokenKind::NonLogicalNewline => {
38+
line_start = token.range().end();
39+
}
40+
_ => {}
2841
}
2942
}
3043

31-
suppressions
44+
Suppressions { file, suppressions }
45+
}
46+
47+
#[derive(Clone, Debug, Eq, PartialEq)]
48+
pub(crate) struct Suppressions {
49+
file: File,
50+
suppressions: IndexVec<SuppressionIndex, Suppression>,
51+
}
52+
53+
impl Suppressions {
54+
pub(crate) fn find_suppression(
55+
&self,
56+
range: TextRange,
57+
id: LintId,
58+
) -> Option<SuppressionIndex> {
59+
let enclosing_index = self.enclosing_suppression(range.end())?;
60+
let enclosed_suppression = &self.suppressions[enclosing_index];
61+
62+
// Ignore the suppression if it doesn't suppress the current lint.
63+
if !enclosed_suppression.suppresses(id) {
64+
return None;
65+
}
66+
67+
Some(enclosing_index)
68+
}
69+
70+
fn enclosing_suppression(&self, offset: TextSize) -> Option<SuppressionIndex> {
71+
self.suppressions
72+
.binary_search_by(|suppression| {
73+
if suppression.suppressed_range.contains(offset) {
74+
Ordering::Equal
75+
} else if suppression.suppressed_range.end() < offset {
76+
Ordering::Less
77+
} else {
78+
Ordering::Greater
79+
}
80+
})
81+
.ok()
82+
}
83+
}
84+
85+
impl std::ops::Index<SuppressionIndex> for Suppressions {
86+
type Output = Suppression;
87+
88+
fn index(&self, index: SuppressionIndex) -> &Self::Output {
89+
&self.suppressions[index]
90+
}
3291
}
3392

3493
#[newtype_index]
@@ -37,14 +96,13 @@ pub(crate) struct SuppressionIndex;
3796
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
3897
pub(crate) struct Suppression {
3998
target: Option<LintId>,
40-
kind: SuppressionKind,
99+
comment_range: TextRange,
100+
suppressed_range: TextRange,
41101
}
42102

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,
103+
impl Suppression {
104+
pub(crate) fn suppresses(&self, tested_id: LintId) -> bool {
105+
// Use `is_none_or` when the MSRV gets updated to 1.82
106+
self.target.is_none() || self.target == Some(tested_id)
107+
}
50108
}

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>

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
16411641
);
16421642
}
16431643
}
1644+
if checker.enabled(Rule::HardcodedPasswordString) {
1645+
if let Some(value) = value.as_deref() {
1646+
flake8_bandit::rules::assign_hardcoded_password_string(
1647+
checker,
1648+
value,
1649+
std::slice::from_ref(target),
1650+
);
1651+
}
1652+
}
16441653
if checker.enabled(Rule::SelfOrClsAssignment) {
16451654
pylint::rules::self_or_cls_assignment(checker, target);
16461655
}

crates/ruff_python_trivia/src/comment_ranges.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
99
use crate::{has_leading_content, has_trailing_content, is_python_whitespace};
1010

1111
/// Stores the ranges of comments sorted by [`TextRange::start`] in increasing order. No two ranges are overlapping.
12-
#[derive(Clone, Default)]
12+
#[derive(Clone, Default, PartialEq, Eq)]
1313
pub struct CommentRanges {
1414
raw: Vec<TextRange>,
1515
}

0 commit comments

Comments
 (0)