Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Suppressing errors with `type: ignore`

Type check errors can be suppressed by a `type: ignore` comment on the same line as the violation.

## Simple `type: ignore`

```py
a = 4 + test # type: ignore
```

## In parenthesized expression

```py
a = (
4 + test # type: ignore
) # fmt: skip
```

## Before opening parentheses

A suppression that applies to all errors before the openign parentheses.
Comment thread
MichaReiser marked this conversation as resolved.
Outdated

```py
a: Test = ( # type: ignore
5
) # fmt: skip
Comment thread
MichaReiser marked this conversation as resolved.
```

## Multiline string

```py
a: int = 4
a = """
This is a multiline string and the suppression is at its end
""" # type: ignore
```

## Line continuations

Suppressions after a line continuation apply to all previous lines.

```py
# fmt: off
a = test \
+ 2 # type: ignore

a = test \
+ a \
+ 2 # type: ignore
```

## Nested comments

TODO: We should support this for better interopability with other suppression comments.

```py
# fmt: off
# error: [unresolved-reference]
a = test \
Comment thread
MichaReiser marked this conversation as resolved.
+ 2 # fmt: skip # type: ignore

a = test \
+ 2 # type: ignore # fmt: skip
```

## Misspelled `type: ignore`

```py
# error: [unresolved-reference]
a = test + 2 # type: ignoree
```

## Invalid - ignore on opening parentheses

`type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses
in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot.

```py
# fmt: off
a = ( # type: ignore
test + 4 # error: [unresolved-reference]
)
```
1 change: 1 addition & 0 deletions crates/red_knot_python_semantic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
mod suppression;
pub(crate) mod symbol;
pub mod types;
mod unpack;
Expand Down
108 changes: 79 additions & 29 deletions crates/red_knot_python_semantic/src/suppression.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,100 @@
use salsa;
use std::cmp::Ordering;

use ruff_db::{files::File, parsed::comment_ranges, source::source_text};
use ruff_python_parser::TokenKind;
use ruff_source_file::LineRanges;
use ruff_text_size::{Ranged, TextRange, TextSize};

use ruff_db::{files::File, parsed::parsed_module, source::source_text};
use ruff_index::{newtype_index, IndexVec};

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

#[salsa::tracked(return_ref)]
pub(crate) fn suppressions(db: &dyn Db, file: File) -> IndexVec<SuppressionIndex, Suppression> {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops. I accidentally commited this file when I rebased https://github.com/astral-sh/ruff/pull/14956/files#diff-b1807b646317ac1945d748f7db40451ac62c582b1bbc049b174e8d98f13d3f22 Probably because it didn't get stashed with git stash. So consider all code in this file as new

let comments = comment_ranges(db.upcast(), file);
pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions {
let source = source_text(db.upcast(), file);
let parsed = parsed_module(db.upcast(), file);

let mut suppressions = IndexVec::default();
let mut line_start = source.bom_start_offset();

for token in parsed.tokens() {
match token.kind() {
TokenKind::Comment => {
let text = &source[token.range()];

let suppressed_range = TextRange::new(line_start, token.end());

for range in comments {
let text = &source[range];

if text.starts_with("# type: ignore") {
suppressions.push(Suppression {
target: None,
kind: SuppressionKind::TypeIgnore,
});
} else if text.starts_with("# knot: ignore") {
suppressions.push(Suppression {
target: None,
kind: SuppressionKind::KnotIgnore,
});
if text.strip_prefix("# type: ignore").is_some_and(|suffix| {
suffix.is_empty() || suffix.starts_with(char::is_whitespace)
}) {
Comment thread
MichaReiser marked this conversation as resolved.
suppressions.push(Suppression { suppressed_range });
}
}
TokenKind::Newline | TokenKind::NonLogicalNewline => {
line_start = token.range().end();
Comment thread
MichaReiser marked this conversation as resolved.
Outdated
}
_ => {}
}
}

suppressions
Suppressions { suppressions }
}

/// The suppression comments of a single file.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Suppressions {
/// The suppressions sorted by the suppressed range.
suppressions: IndexVec<SuppressionIndex, Suppression>,
}

impl Suppressions {
pub(crate) fn find_suppression(
&self,
range: TextRange,
_id: LintId,
) -> Option<SuppressionIndex> {
let enclosing_index = self.enclosing_suppression(range.end())?;

// TODO(micha):
// * Test if the suppression suppresses the passed lint

Some(enclosing_index)
}

fn enclosing_suppression(&self, offset: TextSize) -> Option<SuppressionIndex> {
self.suppressions
.binary_search_by(|suppression| {
if suppression.suppressed_range.contains(offset) {
Ordering::Equal
} else if suppression.suppressed_range.end() < offset {
Ordering::Less
} else {
Ordering::Greater
}
})
.ok()
}
}

impl std::ops::Index<SuppressionIndex> for Suppressions {
type Output = Suppression;

fn index(&self, index: SuppressionIndex) -> &Self::Output {
&self.suppressions[index]
}
}

#[newtype_index]
pub(crate) struct SuppressionIndex;

/// A `type: ignore` or `knot: ignore` suppression comment.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct Suppression {
target: Option<LintId>,
kind: SuppressionKind,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) enum SuppressionKind {
/// A `type: ignore` comment
TypeIgnore,

/// A `knot: ignore` comment
KnotIgnore,
/// The range for which this suppression applies.
/// Most of the time, this is the range of the comment's line.
/// However, there are few cases where the range gets expanted to
Comment thread
MichaReiser marked this conversation as resolved.
Outdated
/// cover multiple lines:
/// * multiline strings: `expr + """multiline\nstring""" # type: ignore`
/// * line continuations: `expr \ + "test" # type: ignore`
suppressed_range: TextRange,
}
10 changes: 10 additions & 0 deletions crates/red_knot_python_semantic/src/types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use ruff_text_size::Ranged;

use crate::{
lint::{LintId, LintMetadata},
suppression::suppressions,
Db,
};

Expand Down Expand Up @@ -74,6 +75,15 @@ impl<'db> InferContext<'db> {
return;
};

let suppressions = suppressions(self.db, self.file);

if suppressions
.find_suppression(node.range(), LintId::of(lint))
.is_some()
{
return;
}

self.report_diagnostic(node, DiagnosticId::Lint(lint.name()), severity, message);
}

Expand Down
24 changes: 24 additions & 0 deletions crates/ruff_index/src/slice.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::vec::IndexVec;
use crate::Idx;
use std::cmp::Ordering;
use std::fmt::{Debug, Formatter};
use std::marker::PhantomData;
use std::ops::{Index, IndexMut, Range};
Expand Down Expand Up @@ -117,6 +118,29 @@ impl<I: Idx, T> IndexSlice<I, T> {
Err(i) => Err(Idx::new(i)),
}
}

#[inline]
pub fn binary_search_by<'a, F>(&'a self, f: F) -> Result<I, I>
where
F: FnMut(&'a T) -> Ordering,
{
match self.raw.binary_search_by(f) {
Ok(i) => Ok(Idx::new(i)),
Err(i) => Err(Idx::new(i)),
}
}

#[inline]
pub fn binary_search_by_key<'a, B, F>(&'a self, key: &B, f: F) -> Result<I, I>
where
F: FnMut(&'a T) -> B,
B: Ord,
{
match self.raw.binary_search_by_key(key, f) {
Ok(i) => Ok(Idx::new(i)),
Err(i) => Err(Idx::new(i)),
}
}
}

impl<I, T> Debug for IndexSlice<I, T>
Expand Down