-
Notifications
You must be signed in to change notification settings - Fork 2k
Basic support for type: ignore comments
#15046
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
25fd417
95eec88
15eb044
5e2cd37
bd1145f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
||
| ```py | ||
| a: Test = ( # type: ignore | ||
| 5 | ||
| ) # fmt: skip | ||
|
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 \ | ||
|
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] | ||
| ) | ||
| ``` | ||
| 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> { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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) | ||
| }) { | ||
|
MichaReiser marked this conversation as resolved.
|
||
| suppressions.push(Suppression { suppressed_range }); | ||
| } | ||
| } | ||
| TokenKind::Newline | TokenKind::NonLogicalNewline => { | ||
| line_start = token.range().end(); | ||
|
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 | ||
|
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, | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.