ruff/crates/ruff_linter/src/suppression.rs

1662 lines
52 KiB
Rust

use compact_str::CompactString;
use core::fmt;
use ruff_db::diagnostic::Diagnostic;
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::token::{TokenKind, Tokens};
use ruff_python_ast::whitespace::indentation;
use std::cell::Cell;
use std::{error::Error, fmt::Formatter};
use thiserror::Error;
use ruff_python_trivia::Cursor;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize, TextSlice};
use smallvec::{SmallVec, smallvec};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::codes::Rule;
use crate::fix::edits::delete_comment;
use crate::preview::is_range_suppressions_enabled;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA, UnusedNOQAKind};
use crate::settings::LinterSettings;
#[derive(Clone, Debug, Eq, PartialEq)]
enum SuppressionAction {
Disable,
Enable,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SuppressionComment {
/// Range containing the entire suppression comment
range: TextRange,
/// The action directive
action: SuppressionAction,
/// Ranges containing the lint codes being suppressed
codes: SmallVec<[TextRange; 2]>,
/// Range containing the reason for the suppression
reason: TextRange,
}
impl SuppressionComment {
/// Return the suppressed codes as strings
fn codes_as_str<'src>(&self, source: &'src str) -> impl Iterator<Item = &'src str> {
self.codes.iter().map(|range| source.slice(range))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PendingSuppressionComment<'a> {
/// How indented an own-line comment is, or None for trailing comments
indent: &'a str,
/// The suppression comment
comment: SuppressionComment,
}
impl PendingSuppressionComment<'_> {
/// Whether the comment "matches" another comment, based on indentation and suppressed codes
/// Expects a "forward search" for matches, ie, will only match if the current comment is a
/// "disable" comment and other is the matching "enable" comment.
fn matches(&self, other: &PendingSuppressionComment, source: &str) -> bool {
self.comment.action == SuppressionAction::Disable
&& other.comment.action == SuppressionAction::Enable
&& self.indent == other.indent
&& self
.comment
.codes_as_str(source)
.eq(other.comment.codes_as_str(source))
}
}
#[derive(Debug)]
pub(crate) struct Suppression {
/// The lint code being suppressed
code: CompactString,
/// Range for which the suppression applies
range: TextRange,
/// Any comments associated with the suppression
comments: SmallVec<[SuppressionComment; 2]>,
/// Whether this suppression actually suppressed a diagnostic
used: Cell<bool>,
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum InvalidSuppressionKind {
/// Trailing suppression not supported
Trailing,
/// No matching enable or disable suppression found
Unmatched,
/// Suppression does not match surrounding indentation
Indentation,
}
#[allow(unused)]
#[derive(Clone, Debug)]
pub(crate) struct InvalidSuppression {
kind: InvalidSuppressionKind,
comment: SuppressionComment,
}
#[allow(unused)]
#[derive(Debug, Default)]
pub struct Suppressions {
/// Valid suppression ranges with associated comments
valid: Vec<Suppression>,
/// Invalid suppression comments
invalid: Vec<InvalidSuppression>,
/// Parse errors from suppression comments
errors: Vec<ParseError>,
}
impl Suppressions {
pub fn from_tokens(settings: &LinterSettings, source: &str, tokens: &Tokens) -> Suppressions {
if is_range_suppressions_enabled(settings) {
let builder = SuppressionsBuilder::new(source);
builder.load_from_tokens(tokens)
} else {
Suppressions::default()
}
}
pub(crate) fn is_empty(&self) -> bool {
self.valid.is_empty()
}
/// Check if a diagnostic is suppressed by any known range suppressions
pub(crate) fn check_diagnostic(&self, diagnostic: &Diagnostic) -> bool {
if self.valid.is_empty() {
return false;
}
let Some(code) = diagnostic.secondary_code() else {
return false;
};
let Some(span) = diagnostic.primary_span() else {
return false;
};
let Some(range) = span.range() else {
return false;
};
for suppression in &self.valid {
if *code == suppression.code.as_str() && suppression.range.contains_range(range) {
suppression.used.set(true);
return true;
}
}
false
}
pub(crate) fn check_suppressions(&self, context: &LintContext, locator: &Locator) {
if !context.any_rule_enabled(&[Rule::UnusedNOQA, Rule::InvalidRuleCode]) {
return;
}
let unused = self
.valid
.iter()
.filter(|suppression| !suppression.used.get());
for suppression in unused {
let Ok(rule) = Rule::from_code(&suppression.code) else {
continue; // TODO: invalid code
};
for comment in &suppression.comments {
let mut range = comment.range;
let edit = if comment.codes.len() == 1 {
delete_comment(comment.range, locator)
} else {
let code_index = comment
.codes
.iter()
.position(|range| locator.slice(range) == suppression.code)
.unwrap();
range = comment.codes[code_index];
let code_range = if code_index < (comment.codes.len() - 1) {
TextRange::new(
comment.codes[code_index].start(),
comment.codes[code_index + 1].start(),
)
} else {
TextRange::new(
comment.codes[code_index - 1].end(),
comment.codes[code_index].end(),
)
};
Edit::range_deletion(code_range)
};
let codes = if context.is_rule_enabled(rule) {
UnusedCodes {
unmatched: vec![suppression.code.to_string()],
..Default::default()
}
} else {
UnusedCodes {
disabled: vec![suppression.code.to_string()],
..Default::default()
}
};
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(codes),
kind: UnusedNOQAKind::Suppression,
},
range,
);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
for error in self
.errors
.iter()
.filter(|error| error.kind == ParseErrorKind::MissingCodes)
{
let mut diagnostic = context.report_diagnostic(
UnusedNOQA {
codes: Some(UnusedCodes::default()),
kind: UnusedNOQAKind::Suppression,
},
error.range,
);
diagnostic.set_fix(Fix::safe_edit(delete_comment(error.range, locator)));
}
}
}
#[derive(Default)]
pub(crate) struct SuppressionsBuilder<'a> {
source: &'a str,
valid: Vec<Suppression>,
invalid: Vec<InvalidSuppression>,
errors: Vec<ParseError>,
pending: Vec<PendingSuppressionComment<'a>>,
}
impl<'a> SuppressionsBuilder<'a> {
pub(crate) fn new(source: &'a str) -> Self {
Self {
source,
..Default::default()
}
}
pub(crate) fn load_from_tokens(mut self, tokens: &Tokens) -> Suppressions {
let default_indent = "";
let mut indents: Vec<&str> = vec![];
// Iterate through tokens, tracking indentation, filtering trailing comments, and then
// looking for matching comments from the previous block when reaching a dedent token.
for (token_index, token) in tokens.iter().enumerate() {
match token.kind() {
TokenKind::Indent => {
indents.push(self.source.slice(token));
}
TokenKind::Dedent => {
self.match_comments(indents.last().copied().unwrap_or_default(), token.range());
indents.pop();
}
TokenKind::Comment => {
let mut parser = SuppressionParser::new(self.source, token.range());
match parser.parse_comment() {
Ok(comment) => {
let indent = indentation(self.source, &comment.range);
let Some(indent) = indent else {
// trailing suppressions are not supported
self.invalid.push(InvalidSuppression {
kind: InvalidSuppressionKind::Trailing,
comment,
});
continue;
};
// comment matches current block's indentation, or precedes an indent/dedent token
if indent == indents.last().copied().unwrap_or_default()
|| tokens[token_index..]
.iter()
.find(|t| !t.kind().is_trivia())
.is_some_and(|t| {
matches!(t.kind(), TokenKind::Dedent | TokenKind::Indent)
})
{
self.pending
.push(PendingSuppressionComment { indent, comment });
} else {
// weirdly indented? ¯\_(ツ)_/¯
self.invalid.push(InvalidSuppression {
kind: InvalidSuppressionKind::Indentation,
comment,
});
}
}
Err(ParseError {
kind: ParseErrorKind::NotASuppression,
..
}) => {}
Err(error) => {
self.errors.push(error);
}
}
}
_ => {}
}
}
self.match_comments(default_indent, TextRange::up_to(self.source.text_len()));
Suppressions {
valid: self.valid,
invalid: self.invalid,
errors: self.errors,
}
}
fn match_comments(&mut self, current_indent: &str, dedent_range: TextRange) {
let mut comment_index = 0;
// for each pending comment, search for matching comments at the same indentation level,
// generate range suppressions for any matches, and then discard any unmatched comments
// from the outgoing indentation block
while comment_index < self.pending.len() {
let comment = &self.pending[comment_index];
// skip comments from an outer indentation level
if comment.indent.text_len() < current_indent.text_len() {
comment_index += 1;
continue;
}
// find the first matching comment
if let Some(other_index) = self.pending[comment_index + 1..]
.iter()
.position(|other| comment.matches(other, self.source))
{
// offset from current candidate
let other_index = comment_index + 1 + other_index;
let other = &self.pending[other_index];
// record a combined range suppression from the matching comments
let combined_range =
TextRange::new(comment.comment.range.start(), other.comment.range.end());
for code in comment.comment.codes_as_str(self.source) {
self.valid.push(Suppression {
code: code.into(),
range: combined_range,
comments: smallvec![comment.comment.clone(), other.comment.clone()],
used: false.into(),
});
}
// remove both comments from further consideration
self.pending.remove(other_index);
self.pending.remove(comment_index);
} else if matches!(comment.comment.action, SuppressionAction::Disable) {
// treat "disable" comments without a matching "enable" as *implicitly* matched
// to the end of the current indentation level
let implicit_range =
TextRange::new(comment.comment.range.start(), dedent_range.end());
for code in comment.comment.codes_as_str(self.source) {
self.valid.push(Suppression {
code: code.into(),
range: implicit_range,
comments: smallvec![comment.comment.clone()],
used: false.into(),
});
}
self.pending.remove(comment_index);
} else {
self.invalid.push(InvalidSuppression {
kind: InvalidSuppressionKind::Unmatched,
comment: self.pending.remove(comment_index).comment.clone(),
});
}
}
}
}
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
enum ParseErrorKind {
#[error("not a suppression comment")]
NotASuppression,
#[error("comment doesn't start with `#`")]
CommentWithoutHash,
#[error("unknown ruff directive")]
UnknownAction,
#[error("missing suppression codes")]
MissingCodes,
#[error("missing closing bracket")]
MissingBracket,
#[error("missing comma between codes")]
MissingComma,
#[error("invalid error code")]
InvalidCode,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ParseError {
kind: ParseErrorKind,
range: TextRange,
}
impl ParseError {
fn new(kind: ParseErrorKind, range: TextRange) -> Self {
Self { kind, range }
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
}
impl Error for ParseError {}
struct SuppressionParser<'src> {
cursor: Cursor<'src>,
range: TextRange,
}
impl<'src> SuppressionParser<'src> {
fn new(source: &'src str, range: TextRange) -> Self {
let cursor = Cursor::new(&source[range]);
Self { cursor, range }
}
fn parse_comment(&mut self) -> Result<SuppressionComment, ParseError> {
self.cursor.start_token();
if !self.cursor.eat_char('#') {
return self.error(ParseErrorKind::CommentWithoutHash);
}
self.eat_whitespace();
let action = self.eat_action()?;
let codes = self.eat_codes()?;
if codes.is_empty() {
return Err(ParseError::new(ParseErrorKind::MissingCodes, self.range));
}
self.eat_whitespace();
let reason = TextRange::new(self.offset(), self.range.end());
Ok(SuppressionComment {
range: self.range,
action,
codes,
reason,
})
}
fn eat_action(&mut self) -> Result<SuppressionAction, ParseError> {
if !self.cursor.as_str().starts_with("ruff") {
return self.error(ParseErrorKind::NotASuppression);
}
self.cursor.skip_bytes("ruff".len());
self.eat_whitespace();
if !self.cursor.eat_char(':') {
return self.error(ParseErrorKind::NotASuppression);
}
self.eat_whitespace();
if self.cursor.as_str().starts_with("disable") {
self.cursor.skip_bytes("disable".len());
Ok(SuppressionAction::Disable)
} else if self.cursor.as_str().starts_with("enable") {
self.cursor.skip_bytes("enable".len());
Ok(SuppressionAction::Enable)
} else if self.cursor.as_str().starts_with("noqa")
|| self.cursor.as_str().starts_with("isort")
{
// alternate suppression variants, ignore for now
self.error(ParseErrorKind::NotASuppression)
} else {
self.error(ParseErrorKind::UnknownAction)
}
}
fn eat_codes(&mut self) -> Result<SmallVec<[TextRange; 2]>, ParseError> {
self.eat_whitespace();
if !self.cursor.eat_char('[') {
return self.error(ParseErrorKind::MissingCodes);
}
let mut codes: SmallVec<[TextRange; 2]> = smallvec![];
loop {
if self.cursor.is_eof() {
return self.error(ParseErrorKind::MissingBracket);
}
self.eat_whitespace();
if self.cursor.eat_char(']') {
break Ok(codes);
}
let code_start = self.offset();
if !self.eat_word() {
return self.error(ParseErrorKind::InvalidCode);
}
codes.push(TextRange::new(code_start, self.offset()));
self.eat_whitespace();
if !self.cursor.eat_char(',') {
if self.cursor.eat_char(']') {
break Ok(codes);
}
return if self.cursor.is_eof() {
self.error(ParseErrorKind::MissingBracket)
} else {
self.error(ParseErrorKind::MissingComma)
};
}
}
}
fn eat_whitespace(&mut self) -> bool {
if self.cursor.eat_if(char::is_whitespace) {
self.cursor.eat_while(char::is_whitespace);
true
} else {
false
}
}
fn eat_word(&mut self) -> bool {
if self.cursor.eat_if(char::is_alphabetic) {
// Allow `:` for better error recovery when someone uses `lint:code` instead of just `code`.
self.cursor
.eat_while(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | ':'));
true
} else {
false
}
}
fn offset(&self) -> TextSize {
self.range.start() + self.range.len() - self.cursor.text_len()
}
fn error<T>(&self, kind: ParseErrorKind) -> Result<T, ParseError> {
Err(ParseError::new(kind, self.range))
}
}
#[cfg(test)]
mod tests {
use std::fmt::{self, Formatter};
use insta::assert_debug_snapshot;
use itertools::Itertools;
use ruff_python_parser::{Mode, ParseOptions, parse};
use ruff_text_size::{TextRange, TextSize};
use similar::DiffableStr;
use crate::{
settings::LinterSettings,
suppression::{
InvalidSuppression, ParseError, Suppression, SuppressionAction, SuppressionComment,
SuppressionParser, Suppressions,
},
};
#[test]
fn no_suppression() {
let source = "
# this is a comment
print('hello')
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r"
Suppressions {
valid: [],
invalid: [],
errors: [],
}
",
);
}
#[test]
fn file_level_suppression() {
let source = "
# ruff: noqa F401
print('hello')
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r"
Suppressions {
valid: [],
invalid: [],
errors: [],
}
",
);
}
#[test]
fn single_range_suppression() {
let source = "
# ruff: disable[foo]
print('hello')
# ruff: enable[foo]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo]\nprint('hello')\n# ruff: enable[foo]",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[foo]",
action: Enable,
codes: [
"foo",
],
reason: "",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn single_range_suppression_implicit_match() {
let source = "
# ruff: disable[foo]
print('hello')
def foo():
# ruff: disable[bar]
print('hello')
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[bar]\n print('hello')\n\n",
code: "bar",
comments: [
SuppressionComment {
text: "# ruff: disable[bar]",
action: Disable,
codes: [
"bar",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[foo]\nprint('hello')\n\ndef foo():\n # ruff: disable[bar]\n print('hello')\n\n",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn nested_range_suppressions() {
let source = "
class Foo:
# ruff: disable[foo]
def bar(self):
# ruff: disable[bar]
print('hello')
# ruff: enable[bar]
# ruff: enable[foo]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[bar]\n print('hello')\n # ruff: enable[bar]",
code: "bar",
comments: [
SuppressionComment {
text: "# ruff: disable[bar]",
action: Disable,
codes: [
"bar",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[bar]",
action: Enable,
codes: [
"bar",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[foo]\n def bar(self):\n # ruff: disable[bar]\n print('hello')\n # ruff: enable[bar]\n # ruff: enable[foo]",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[foo]",
action: Enable,
codes: [
"foo",
],
reason: "",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn interleaved_range_suppressions() {
let source = "
def foo():
# ruff: disable[foo]
print('hello')
# ruff: disable[bar]
print('hello')
# ruff: enable[foo]
print('hello')
# ruff: enable[bar]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo]\n print('hello')\n # ruff: disable[bar]\n print('hello')\n # ruff: enable[foo]",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[foo]",
action: Enable,
codes: [
"foo",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[bar]\n print('hello')\n # ruff: enable[foo]\n print('hello')\n # ruff: enable[bar]",
code: "bar",
comments: [
SuppressionComment {
text: "# ruff: disable[bar]",
action: Disable,
codes: [
"bar",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[bar]",
action: Enable,
codes: [
"bar",
],
reason: "",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn range_suppression_two_codes() {
let source = "
# ruff: disable[foo, bar]
print('hello')
# ruff: enable[foo, bar]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[foo, bar]",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo, bar]",
action: Disable,
codes: [
"foo",
"bar",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[foo, bar]",
action: Enable,
codes: [
"foo",
"bar",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[foo, bar]",
code: "bar",
comments: [
SuppressionComment {
text: "# ruff: disable[foo, bar]",
action: Disable,
codes: [
"foo",
"bar",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[foo, bar]",
action: Enable,
codes: [
"foo",
"bar",
],
reason: "",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn range_suppression_unmatched() {
let source = "
# ruff: disable[foo]
print('hello')
# ruff: enable[bar]
print('world')
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo]\nprint('hello')\n# ruff: enable[bar]\nprint('world')\n",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
],
},
],
invalid: [
InvalidSuppression {
kind: Unmatched,
comment: SuppressionComment {
text: "# ruff: enable[bar]",
action: Enable,
codes: [
"bar",
],
reason: "",
},
},
],
errors: [],
}
"##,
);
}
#[test]
fn range_suppression_unordered() {
let source = "
# ruff: disable[foo, bar]
print('hello')
# ruff: enable[bar, foo]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[bar, foo]\n",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo, bar]",
action: Disable,
codes: [
"foo",
"bar",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[foo, bar]\nprint('hello')\n# ruff: enable[bar, foo]\n",
code: "bar",
comments: [
SuppressionComment {
text: "# ruff: disable[foo, bar]",
action: Disable,
codes: [
"foo",
"bar",
],
reason: "",
},
],
},
],
invalid: [
InvalidSuppression {
kind: Unmatched,
comment: SuppressionComment {
text: "# ruff: enable[bar, foo]",
action: Enable,
codes: [
"bar",
"foo",
],
reason: "",
},
},
],
errors: [],
}
"##,
);
}
#[test]
fn range_suppression_extra_disable() {
let source = "
# ruff: disable[foo] first
print('hello')
# ruff: disable[foo] second
print('hello')
# ruff: enable[foo]
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[foo] first\nprint('hello')\n# ruff: disable[foo] second\nprint('hello')\n# ruff: enable[foo]",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo] first",
action: Disable,
codes: [
"foo",
],
reason: "first",
},
SuppressionComment {
text: "# ruff: enable[foo]",
action: Enable,
codes: [
"foo",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[foo] second\nprint('hello')\n# ruff: enable[foo]\n",
code: "foo",
comments: [
SuppressionComment {
text: "# ruff: disable[foo] second",
action: Disable,
codes: [
"foo",
],
reason: "second",
},
],
},
],
invalid: [],
errors: [],
}
"##,
);
}
#[test]
fn combined_range_suppressions() {
let source = "
# ruff: noqa # ignored
# comment here
print('hello') # ruff: disable[phi] trailing
# ruff: disable[alpha]
def foo():
# ruff: disable[beta,gamma]
if True:
# ruff: disable[delta] unmatched
pass
# ruff: enable[beta,gamma]
# ruff: enable[alpha]
# ruff: disable # parse error!
def bar():
# ruff: disable[zeta] unmatched
pass
# ruff: enable[zeta] underindented
pass
";
assert_debug_snapshot!(
Suppressions::debug(source),
@r##"
Suppressions {
valid: [
Suppression {
covered_source: "# ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]\n# ruff: enable[alpha]\n\n# ruff: disable # parse error!\n",
code: "delta",
comments: [
SuppressionComment {
text: "# ruff: disable[delta] unmatched",
action: Disable,
codes: [
"delta",
],
reason: "unmatched",
},
],
},
Suppression {
covered_source: "# ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]",
code: "beta",
comments: [
SuppressionComment {
text: "# ruff: disable[beta,gamma]",
action: Disable,
codes: [
"beta",
"gamma",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[beta,gamma]",
action: Enable,
codes: [
"beta",
"gamma",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]",
code: "gamma",
comments: [
SuppressionComment {
text: "# ruff: disable[beta,gamma]",
action: Disable,
codes: [
"beta",
"gamma",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[beta,gamma]",
action: Enable,
codes: [
"beta",
"gamma",
],
reason: "",
},
],
},
Suppression {
covered_source: "# ruff: disable[zeta] unmatched\n pass\n# ruff: enable[zeta] underindented\n pass\n",
code: "zeta",
comments: [
SuppressionComment {
text: "# ruff: disable[zeta] unmatched",
action: Disable,
codes: [
"zeta",
],
reason: "unmatched",
},
],
},
Suppression {
covered_source: "# ruff: disable[alpha]\ndef foo():\n # ruff: disable[beta,gamma]\n if True:\n # ruff: disable[delta] unmatched\n pass\n # ruff: enable[beta,gamma]\n# ruff: enable[alpha]",
code: "alpha",
comments: [
SuppressionComment {
text: "# ruff: disable[alpha]",
action: Disable,
codes: [
"alpha",
],
reason: "",
},
SuppressionComment {
text: "# ruff: enable[alpha]",
action: Enable,
codes: [
"alpha",
],
reason: "",
},
],
},
],
invalid: [
InvalidSuppression {
kind: Trailing,
comment: SuppressionComment {
text: "# ruff: disable[phi] trailing",
action: Disable,
codes: [
"phi",
],
reason: "trailing",
},
},
InvalidSuppression {
kind: Indentation,
comment: SuppressionComment {
text: "# ruff: enable[zeta] underindented",
action: Enable,
codes: [
"zeta",
],
reason: "underindented",
},
},
],
errors: [
ParseError {
text: "# ruff: disable # parse error!",
kind: MissingCodes,
},
],
}
"##,
);
}
#[test]
fn parse_unrelated_comment() {
assert_debug_snapshot!(
parse_suppression_comment("# hello world"),
@r"
Err(
ParseError {
kind: NotASuppression,
range: 0..13,
},
)
",
);
}
#[test]
fn parse_invalid_action() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: lol[hi]"),
@r"
Err(
ParseError {
kind: UnknownAction,
range: 0..15,
},
)
",
);
}
#[test]
fn parse_missing_codes() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable"),
@r"
Err(
ParseError {
kind: MissingCodes,
range: 0..15,
},
)
",
);
}
#[test]
fn parse_empty_codes() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[]"),
@r"
Err(
ParseError {
kind: MissingCodes,
range: 0..17,
},
)
",
);
}
#[test]
fn parse_missing_bracket() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[foo"),
@r"
Err(
ParseError {
kind: MissingBracket,
range: 0..19,
},
)
",
);
}
#[test]
fn parse_missing_comma() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[foo bar]"),
@r"
Err(
ParseError {
kind: MissingComma,
range: 0..24,
},
)
",
);
}
#[test]
fn disable_single_code() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[foo]"),
@r##"
Ok(
SuppressionComment {
text: "# ruff: disable[foo]",
action: Disable,
codes: [
"foo",
],
reason: "",
},
)
"##,
);
}
#[test]
fn disable_single_code_with_reason() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[foo] I like bar better"),
@r##"
Ok(
SuppressionComment {
text: "# ruff: disable[foo] I like bar better",
action: Disable,
codes: [
"foo",
],
reason: "I like bar better",
},
)
"##,
);
}
#[test]
fn disable_multiple_codes() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: disable[foo, bar]"),
@r##"
Ok(
SuppressionComment {
text: "# ruff: disable[foo, bar]",
action: Disable,
codes: [
"foo",
"bar",
],
reason: "",
},
)
"##,
);
}
#[test]
fn enable_single_code() {
assert_debug_snapshot!(
parse_suppression_comment("# ruff: enable[some-thing]"),
@r##"
Ok(
SuppressionComment {
text: "# ruff: enable[some-thing]",
action: Enable,
codes: [
"some-thing",
],
reason: "",
},
)
"##,
);
}
#[test]
fn trailing_comment() {
let source = "print('hello world') # ruff: enable[some-thing]";
let comment = parse_suppression_comment(source);
assert_debug_snapshot!(
comment,
@r##"
Ok(
SuppressionComment {
text: "# ruff: enable[some-thing]",
action: Enable,
codes: [
"some-thing",
],
reason: "",
},
)
"##,
);
}
#[test]
fn indented_comment() {
let source = " # ruff: enable[some-thing]";
let comment = parse_suppression_comment(source);
assert_debug_snapshot!(
comment,
@r##"
Ok(
SuppressionComment {
text: "# ruff: enable[some-thing]",
action: Enable,
codes: [
"some-thing",
],
reason: "",
},
)
"##,
);
}
#[test]
fn comment_attributes() {
let source = "# ruff: disable[foo, bar] hello world";
let mut parser = SuppressionParser::new(
source,
TextRange::new(0.into(), TextSize::try_from(source.len()).unwrap()),
);
let comment = parser.parse_comment().unwrap();
assert_eq!(comment.action, SuppressionAction::Disable);
assert_eq!(
comment
.codes
.into_iter()
.map(|range| { source.slice(range.into()) })
.collect::<Vec<_>>(),
["foo", "bar"]
);
assert_eq!(source.slice(comment.reason.into()), "hello world");
}
/// Parse a single suppression comment for testing
fn parse_suppression_comment(
source: &'_ str,
) -> Result<DebugSuppressionComment<'_>, ParseError> {
let offset = TextSize::new(source.find('#').unwrap_or(0).try_into().unwrap());
let mut parser = SuppressionParser::new(
source,
TextRange::new(offset, TextSize::try_from(source.len()).unwrap()),
);
match parser.parse_comment() {
Ok(comment) => Ok(DebugSuppressionComment { source, comment }),
Err(error) => Err(error),
}
}
impl Suppressions {
/// Parse all suppressions and errors in a module for testing
fn debug(source: &'_ str) -> DebugSuppressions<'_> {
let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap();
let suppressions = Suppressions::from_tokens(
&LinterSettings::default().with_preview_mode(),
source,
parsed.tokens(),
);
DebugSuppressions {
source,
suppressions,
}
}
}
struct DebugSuppressions<'a> {
source: &'a str,
suppressions: Suppressions,
}
impl fmt::Debug for DebugSuppressions<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Suppressions")
.field(
"valid",
&self
.suppressions
.valid
.iter()
.map(|suppression| DebugSuppression {
source: self.source,
suppression,
})
.collect_vec(),
)
.field(
"invalid",
&self
.suppressions
.invalid
.iter()
.map(|invalid| DebugInvalidSuppression {
source: self.source,
invalid,
})
.collect_vec(),
)
.field(
"errors",
&self
.suppressions
.errors
.iter()
.map(|error| DebugParseError {
source: self.source,
error,
})
.collect_vec(),
)
.finish()
}
}
struct DebugSuppression<'a> {
source: &'a str,
suppression: &'a Suppression,
}
impl fmt::Debug for DebugSuppression<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Suppression")
.field("covered_source", &&self.source[self.suppression.range])
.field("code", &self.suppression.code)
.field(
"comments",
&self
.suppression
.comments
.iter()
.map(|comment| DebugSuppressionComment {
source: self.source,
comment: comment.clone(),
})
.collect_vec(),
)
.finish()
}
}
struct DebugInvalidSuppression<'a> {
source: &'a str,
invalid: &'a InvalidSuppression,
}
impl fmt::Debug for DebugInvalidSuppression<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidSuppression")
.field("kind", &self.invalid.kind)
.field(
"comment",
&DebugSuppressionComment {
source: self.source,
comment: self.invalid.comment.clone(),
},
)
.finish()
}
}
struct DebugParseError<'a> {
source: &'a str,
error: &'a ParseError,
}
impl fmt::Debug for DebugParseError<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ParseError")
.field("text", &&self.source[self.error.range])
.field("kind", &self.error.kind)
.finish()
}
}
struct DebugSuppressionComment<'a> {
source: &'a str,
comment: SuppressionComment,
}
impl fmt::Debug for DebugSuppressionComment<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("SuppressionComment")
.field("text", &&self.source[self.comment.range])
.field("action", &self.comment.action)
.field(
"codes",
&DebugCodes {
source: self.source,
codes: &self.comment.codes,
},
)
.field("reason", &&self.source[self.comment.reason])
.finish()
}
}
struct DebugCodes<'a> {
source: &'a str,
codes: &'a [TextRange],
}
impl fmt::Debug for DebugCodes<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut f = f.debug_list();
for code in self.codes {
f.entry(&&self.source[*code]);
}
f.finish()
}
}
}