diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF043.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF043.py new file mode 100644 index 0000000000000..6b7602ce62f7c --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF043.py @@ -0,0 +1,91 @@ +import re +import pytest + +def test_foo(): + + ### Errors + + with pytest.raises(FooAtTheEnd, match="foo."): ... + with pytest.raises(PackageExtraSpecifier, match="Install `foo[bar]` to enjoy all features"): ... + with pytest.raises(InnocentQuestion, match="Did you mean to use `Literal` instead?"): ... + + with pytest.raises(StringConcatenation, match="Huh" + "?"): ... + with pytest.raises(ManuallyEscapedWindowsPathToDotFile, match="C:\\\\Users\\\\Foo\\\\.config"): ... + + with pytest.raises(MiddleDot, match="foo.bar"): ... + with pytest.raises(EndDot, match="foobar."): ... + with pytest.raises(EscapedFollowedByUnescaped, match="foo\\.*bar"): ... + with pytest.raises(UnescapedFollowedByEscaped, match="foo.\\*bar"): ... + + + ## Metasequences + + with pytest.raises(StartOfInput, match="foo\\Abar"): ... + with pytest.raises(WordBoundary, match="foo\\bbar"): ... + with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... + with pytest.raises(Digit, match="foo\\dbar"): ... + with pytest.raises(NonDigit, match="foo\\Dbar"): ... + with pytest.raises(Whitespace, match="foo\\sbar"): ... + with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... + with pytest.raises(WordCharacter, match="foo\\wbar"): ... + with pytest.raises(NonWordCharacter, match="foo\\Wbar"): ... + with pytest.raises(EndOfInput, match="foo\\zbar"): ... + + with pytest.raises(StartOfInput2, match="foobar\\A"): ... + with pytest.raises(WordBoundary2, match="foobar\\b"): ... + with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... + with pytest.raises(Digit2, match="foobar\\d"): ... + with pytest.raises(NonDigit2, match="foobar\\D"): ... + with pytest.raises(Whitespace2, match="foobar\\s"): ... + with pytest.raises(NonWhitespace2, match="foobar\\S"): ... + with pytest.raises(WordCharacter2, match="foobar\\w"): ... + with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... + with pytest.raises(EndOfInput2, match="foobar\\z"): ... + + + ### Acceptable false positives + + with pytest.raises(NameEscape, match="\\N{EN DASH}"): ... + + + ### No errors + + with pytest.raises(NoMatch): ... + with pytest.raises(NonLiteral, match=pattern): ... + with pytest.raises(FunctionCall, match=frobnicate("qux")): ... + with pytest.raises(ReEscaped, match=re.escape("foobar")): ... + with pytest.raises(RawString, match=r"fo()bar"): ... + with pytest.raises(RawStringPart, match=r"foo" '\bar'): ... + with pytest.raises(NoMetacharacters, match="foobar"): ... + with pytest.raises(EndBackslash, match="foobar\\"): ... + + with pytest.raises(ManuallyEscaped, match="some\\.fully\\.qualified\\.name"): ... + with pytest.raises(ManuallyEscapedWindowsPath, match="C:\\\\Users\\\\Foo\\\\file\\.py"): ... + + with pytest.raises(MiddleEscapedDot, match="foo\\.bar"): ... + with pytest.raises(MiddleEscapedBackslash, match="foo\\\\bar"): ... + with pytest.raises(EndEscapedDot, match="foobar\\."): ... + with pytest.raises(EndEscapedBackslash, match="foobar\\\\"): ... + + + ## Not-so-special metasequences + + with pytest.raises(Alert, match="\\f"): ... + with pytest.raises(FormFeed, match="\\f"): ... + with pytest.raises(Newline, match="\\n"): ... + with pytest.raises(CarriageReturn, match="\\r"): ... + with pytest.raises(Tab, match="\\t"): ... + with pytest.raises(VerticalTab, match="\\v"): ... + with pytest.raises(HexEscape, match="\\xFF"): ... + with pytest.raises(_16BitUnicodeEscape, match="\\FFFF"): ... + with pytest.raises(_32BitUnicodeEscape, match="\\0010FFFF"): ... + + ## Escaped metasequences + + with pytest.raises(Whitespace, match="foo\\\\sbar"): ... + with pytest.raises(NonWhitespace, match="foo\\\\Sbar"): ... + + ## Work by accident + + with pytest.raises(OctalEscape, match="\\042"): ... diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 4de46b84c40e0..a3d753750f4e3 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -1105,6 +1105,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::BatchedWithoutExplicitStrict) { flake8_bugbear::rules::batched_without_explicit_strict(checker, call); } + if checker.enabled(Rule::PytestRaisesAmbiguousPattern) { + ruff::rules::pytest_raises_ambiguous_pattern(checker, call); + } } Expr::Dict(dict) => { if checker.any_enabled(&[ diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 13549898308d6..c14eb0a524655 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -985,6 +985,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern), (Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument), (Ruff, "041") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryNestedLiteral), + (Ruff, "043") => (RuleGroup::Preview, rules::ruff::rules::PytestRaisesAmbiguousPattern), (Ruff, "046") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryCastToInt), (Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing), (Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel), diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs index f6fc9beea6d33..ba7874c1e0c18 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/assertion.rs @@ -144,7 +144,7 @@ impl Violation for PytestAssertInExcept { /// ... /// ``` /// -/// References +/// ## References /// - [`pytest` documentation: `pytest.fail`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fail) #[derive(ViolationMetadata)] pub(crate) struct PytestAssertAlwaysFalse; diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs index 761513caf6dcb..8365526a566fd 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs @@ -104,7 +104,6 @@ impl AlwaysFixableViolation for PytestIncorrectMarkParenthesesStyle { /// /// ## References /// - [`pytest` documentation: `pytest.mark.usefixtures`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-mark-usefixtures) - #[derive(ViolationMetadata)] pub(crate) struct PytestUseFixturesWithoutParameters; diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs index 42ae92e68d82c..82972ef54bb53 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/raises.rs @@ -151,7 +151,7 @@ impl Violation for PytestRaisesWithoutException { } } -fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { +pub(crate) fn is_pytest_raises(func: &Expr, semantic: &SemanticModel) -> bool { semantic .resolve_qualified_name(func) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "raises"])) diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index d2de6345cb89f..d883f5a0d2bc9 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -415,6 +415,7 @@ mod tests { #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_0.py"))] #[test_case(Rule::UnnecessaryRegularExpression, Path::new("RUF055_1.py"))] #[test_case(Rule::UnnecessaryCastToInt, Path::new("RUF046.py"))] + #[test_case(Rule::PytestRaisesAmbiguousPattern, Path::new("RUF043.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index b83156e3d6f09..e9e74d1a534af 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -23,6 +23,7 @@ pub(crate) use never_union::*; pub(crate) use none_not_at_end_of_union::*; pub(crate) use parenthesize_chained_operators::*; pub(crate) use post_init_default::*; +pub(crate) use pytest_raises_ambiguous_pattern::*; pub(crate) use quadratic_list_summation::*; pub(crate) use redirected_noqa::*; pub(crate) use redundant_bool_literal::*; @@ -71,6 +72,7 @@ mod never_union; mod none_not_at_end_of_union; mod parenthesize_chained_operators; mod post_init_default; +mod pytest_raises_ambiguous_pattern; mod quadratic_list_summation; mod redirected_noqa; mod redundant_bool_literal; diff --git a/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs new file mode 100644 index 0000000000000..117b782b170e6 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/pytest_raises_ambiguous_pattern.rs @@ -0,0 +1,155 @@ +use crate::checkers::ast::Checker; +use crate::rules::flake8_pytest_style::rules::is_pytest_raises; +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, ViolationMetadata}; +use ruff_python_ast as ast; + +/// ## What it does +/// Checks for non-raw literal string arguments passed to the `match` parameter +/// of `pytest.raises()` where the string contains at least one unescaped +/// regex metacharacter. +/// +/// ## Why is this bad? +/// The `match` argument is implicitly converted to a regex under the hood. +/// It should be made explicit whether the string is meant to be a regex or a "plain" pattern +/// by prefixing the string with the `r` suffix, escaping the metacharacter(s) +/// in the string using backslashes, or wrapping the entire string in a call to +/// `re.escape()`. +/// +/// ## Example +/// +/// ```python +/// import pytest +/// +/// +/// with pytest.raises(Exception, match="A full sentence."): +/// do_thing_that_raises() +/// ``` +/// +/// Use instead: +/// +/// ```python +/// import pytest +/// +/// +/// with pytest.raises(Exception, match=r"A full sentence."): +/// do_thing_that_raises() +/// ``` +/// +/// Alternatively: +/// +/// ```python +/// import pytest +/// import re +/// +/// +/// with pytest.raises(Exception, match=re.escape("A full sentence.")): +/// do_thing_that_raises() +/// ``` +/// +/// or: +/// +/// ```python +/// import pytest +/// import re +/// +/// +/// with pytest.raises(Exception, "A full sentence\\."): +/// do_thing_that_raises() +/// ``` +/// +/// ## References +/// - [Python documentation: `re.escape`](https://docs.python.org/3/library/re.html#re.escape) +/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) +#[derive(ViolationMetadata)] +pub(crate) struct PytestRaisesAmbiguousPattern; + +impl Violation for PytestRaisesAmbiguousPattern { + #[derive_message_formats] + fn message(&self) -> String { + "Pattern passed to `match=` contains metacharacters but is neither escaped nor raw" + .to_string() + } + + fn fix_title(&self) -> Option { + Some("Use a raw string or `re.escape()` to make the intention explicit".to_string()) + } +} + +/// RUF043 +pub(crate) fn pytest_raises_ambiguous_pattern(checker: &mut Checker, call: &ast::ExprCall) { + if !is_pytest_raises(&call.func, checker.semantic()) { + return; + } + + // It *can* be passed as a positional argument if you try very hard, + // but pytest only documents it as a keyword argument, and it's quite hard pass it positionally + let Some(ast::Keyword { value, .. }) = call.arguments.find_keyword("match") else { + return; + }; + + let ast::Expr::StringLiteral(string) = value else { + return; + }; + + let any_part_is_raw = string.value.iter().any(|part| part.flags.prefix().is_raw()); + + if any_part_is_raw || !string_has_unescaped_metacharacters(&string.value) { + return; + } + + let diagnostic = Diagnostic::new(PytestRaisesAmbiguousPattern, string.range); + + checker.diagnostics.push(diagnostic); +} + +fn string_has_unescaped_metacharacters(value: &ast::StringLiteralValue) -> bool { + let mut escaped = false; + + for character in value.chars() { + if escaped { + if escaped_char_is_regex_metasequence(character) { + return true; + } + + escaped = false; + continue; + } + + if character == '\\' { + escaped = true; + continue; + } + + if char_is_regex_metacharacter(character) { + return true; + } + } + + false +} + +/// Whether the sequence `\` means anything special: +/// +/// * `\A`: Start of input +/// * `\b`, `\B`: Word boundary and non-word-boundary +/// * `\d`, `\D`: Digit and non-digit +/// * `\s`, `\S`: Whitespace and non-whitespace +/// * `\w`, `\W`: Word and non-word character +/// * `\z`: End of input +/// +/// `\u`, `\U`, `\N`, `\x`, `\a`, `\f`, `\n`, `\r`, `\t`, `\v` +/// are also valid in normal strings and thus do not count. +/// `\b` means backspace only in character sets, +/// while backreferences (e.g., `\1`) are not valid without groups, +/// both of which should be caught in [`string_has_unescaped_metacharacters`]. +const fn escaped_char_is_regex_metasequence(c: char) -> bool { + matches!(c, 'A' | 'b' | 'B' | 'd' | 'D' | 's' | 'S' | 'w' | 'W' | 'z') +} + +const fn char_is_regex_metacharacter(c: char) -> bool { + matches!( + c, + '.' | '^' | '$' | '*' | '+' | '?' | '{' | '[' | '\\' | '|' | '(' + ) +} diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF043_RUF043.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF043_RUF043.py.snap new file mode 100644 index 0000000000000..d8c854ca1210c --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__preview__RUF043_RUF043.py.snap @@ -0,0 +1,319 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF043.py:8:43: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | + 6 | ### Errors + 7 | + 8 | with pytest.raises(FooAtTheEnd, match="foo."): ... + | ^^^^^^ RUF043 + 9 | with pytest.raises(PackageExtraSpecifier, match="Install `foo[bar]` to enjoy all features"): ... +10 | with pytest.raises(InnocentQuestion, match="Did you mean to use `Literal` instead?"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:9:53: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | + 8 | with pytest.raises(FooAtTheEnd, match="foo."): ... + 9 | with pytest.raises(PackageExtraSpecifier, match="Install `foo[bar]` to enjoy all features"): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF043 +10 | with pytest.raises(InnocentQuestion, match="Did you mean to use `Literal` instead?"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:10:48: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | + 8 | with pytest.raises(FooAtTheEnd, match="foo."): ... + 9 | with pytest.raises(PackageExtraSpecifier, match="Install `foo[bar]` to enjoy all features"): ... +10 | with pytest.raises(InnocentQuestion, match="Did you mean to use `Literal` instead?"): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF043 +11 | +12 | with pytest.raises(StringConcatenation, match="Huh" + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:12:51: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +10 | with pytest.raises(InnocentQuestion, match="Did you mean to use `Literal` instead?"): ... +11 | +12 | with pytest.raises(StringConcatenation, match="Huh" + | ___________________________________________________^ +13 | | "?"): ... + | |_____________________________________________________^ RUF043 +14 | with pytest.raises(ManuallyEscapedWindowsPathToDotFile, match="C:\\\\Users\\\\Foo\\\\.config"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:14:67: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +12 | with pytest.raises(StringConcatenation, match="Huh" +13 | "?"): ... +14 | with pytest.raises(ManuallyEscapedWindowsPathToDotFile, match="C:\\\\Users\\\\Foo\\\\.config"): ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF043 +15 | +16 | with pytest.raises(MiddleDot, match="foo.bar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:16:41: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +14 | with pytest.raises(ManuallyEscapedWindowsPathToDotFile, match="C:\\\\Users\\\\Foo\\\\.config"): ... +15 | +16 | with pytest.raises(MiddleDot, match="foo.bar"): ... + | ^^^^^^^^^ RUF043 +17 | with pytest.raises(EndDot, match="foobar."): ... +18 | with pytest.raises(EscapedFollowedByUnescaped, match="foo\\.*bar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:17:38: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +16 | with pytest.raises(MiddleDot, match="foo.bar"): ... +17 | with pytest.raises(EndDot, match="foobar."): ... + | ^^^^^^^^^ RUF043 +18 | with pytest.raises(EscapedFollowedByUnescaped, match="foo\\.*bar"): ... +19 | with pytest.raises(UnescapedFollowedByEscaped, match="foo.\\*bar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:18:58: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +16 | with pytest.raises(MiddleDot, match="foo.bar"): ... +17 | with pytest.raises(EndDot, match="foobar."): ... +18 | with pytest.raises(EscapedFollowedByUnescaped, match="foo\\.*bar"): ... + | ^^^^^^^^^^^^ RUF043 +19 | with pytest.raises(UnescapedFollowedByEscaped, match="foo.\\*bar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:19:58: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +17 | with pytest.raises(EndDot, match="foobar."): ... +18 | with pytest.raises(EscapedFollowedByUnescaped, match="foo\\.*bar"): ... +19 | with pytest.raises(UnescapedFollowedByEscaped, match="foo.\\*bar"): ... + | ^^^^^^^^^^^^ RUF043 + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:24:44: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +22 | ## Metasequences +23 | +24 | with pytest.raises(StartOfInput, match="foo\\Abar"): ... + | ^^^^^^^^^^^ RUF043 +25 | with pytest.raises(WordBoundary, match="foo\\bbar"): ... +26 | with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:25:44: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +24 | with pytest.raises(StartOfInput, match="foo\\Abar"): ... +25 | with pytest.raises(WordBoundary, match="foo\\bbar"): ... + | ^^^^^^^^^^^ RUF043 +26 | with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... +27 | with pytest.raises(Digit, match="foo\\dbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:26:47: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +24 | with pytest.raises(StartOfInput, match="foo\\Abar"): ... +25 | with pytest.raises(WordBoundary, match="foo\\bbar"): ... +26 | with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... + | ^^^^^^^^^^^ RUF043 +27 | with pytest.raises(Digit, match="foo\\dbar"): ... +28 | with pytest.raises(NonDigit, match="foo\\Dbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:27:37: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +25 | with pytest.raises(WordBoundary, match="foo\\bbar"): ... +26 | with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... +27 | with pytest.raises(Digit, match="foo\\dbar"): ... + | ^^^^^^^^^^^ RUF043 +28 | with pytest.raises(NonDigit, match="foo\\Dbar"): ... +29 | with pytest.raises(Whitespace, match="foo\\sbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:28:40: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +26 | with pytest.raises(NonWordBoundary, match="foo\\Bbar"): ... +27 | with pytest.raises(Digit, match="foo\\dbar"): ... +28 | with pytest.raises(NonDigit, match="foo\\Dbar"): ... + | ^^^^^^^^^^^ RUF043 +29 | with pytest.raises(Whitespace, match="foo\\sbar"): ... +30 | with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:29:42: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +27 | with pytest.raises(Digit, match="foo\\dbar"): ... +28 | with pytest.raises(NonDigit, match="foo\\Dbar"): ... +29 | with pytest.raises(Whitespace, match="foo\\sbar"): ... + | ^^^^^^^^^^^ RUF043 +30 | with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... +31 | with pytest.raises(WordCharacter, match="foo\\wbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:30:45: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +28 | with pytest.raises(NonDigit, match="foo\\Dbar"): ... +29 | with pytest.raises(Whitespace, match="foo\\sbar"): ... +30 | with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... + | ^^^^^^^^^^^ RUF043 +31 | with pytest.raises(WordCharacter, match="foo\\wbar"): ... +32 | with pytest.raises(NonWordCharacter, match="foo\\Wbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:31:45: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +29 | with pytest.raises(Whitespace, match="foo\\sbar"): ... +30 | with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... +31 | with pytest.raises(WordCharacter, match="foo\\wbar"): ... + | ^^^^^^^^^^^ RUF043 +32 | with pytest.raises(NonWordCharacter, match="foo\\Wbar"): ... +33 | with pytest.raises(EndOfInput, match="foo\\zbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:32:48: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +30 | with pytest.raises(NonWhitespace, match="foo\\Sbar"): ... +31 | with pytest.raises(WordCharacter, match="foo\\wbar"): ... +32 | with pytest.raises(NonWordCharacter, match="foo\\Wbar"): ... + | ^^^^^^^^^^^ RUF043 +33 | with pytest.raises(EndOfInput, match="foo\\zbar"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:33:42: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +31 | with pytest.raises(WordCharacter, match="foo\\wbar"): ... +32 | with pytest.raises(NonWordCharacter, match="foo\\Wbar"): ... +33 | with pytest.raises(EndOfInput, match="foo\\zbar"): ... + | ^^^^^^^^^^^ RUF043 +34 | +35 | with pytest.raises(StartOfInput2, match="foobar\\A"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:35:45: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +33 | with pytest.raises(EndOfInput, match="foo\\zbar"): ... +34 | +35 | with pytest.raises(StartOfInput2, match="foobar\\A"): ... + | ^^^^^^^^^^^ RUF043 +36 | with pytest.raises(WordBoundary2, match="foobar\\b"): ... +37 | with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:36:45: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +35 | with pytest.raises(StartOfInput2, match="foobar\\A"): ... +36 | with pytest.raises(WordBoundary2, match="foobar\\b"): ... + | ^^^^^^^^^^^ RUF043 +37 | with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... +38 | with pytest.raises(Digit2, match="foobar\\d"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:37:48: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +35 | with pytest.raises(StartOfInput2, match="foobar\\A"): ... +36 | with pytest.raises(WordBoundary2, match="foobar\\b"): ... +37 | with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... + | ^^^^^^^^^^^ RUF043 +38 | with pytest.raises(Digit2, match="foobar\\d"): ... +39 | with pytest.raises(NonDigit2, match="foobar\\D"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:38:38: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +36 | with pytest.raises(WordBoundary2, match="foobar\\b"): ... +37 | with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... +38 | with pytest.raises(Digit2, match="foobar\\d"): ... + | ^^^^^^^^^^^ RUF043 +39 | with pytest.raises(NonDigit2, match="foobar\\D"): ... +40 | with pytest.raises(Whitespace2, match="foobar\\s"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:39:41: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +37 | with pytest.raises(NonWordBoundary2, match="foobar\\B"): ... +38 | with pytest.raises(Digit2, match="foobar\\d"): ... +39 | with pytest.raises(NonDigit2, match="foobar\\D"): ... + | ^^^^^^^^^^^ RUF043 +40 | with pytest.raises(Whitespace2, match="foobar\\s"): ... +41 | with pytest.raises(NonWhitespace2, match="foobar\\S"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:40:43: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +38 | with pytest.raises(Digit2, match="foobar\\d"): ... +39 | with pytest.raises(NonDigit2, match="foobar\\D"): ... +40 | with pytest.raises(Whitespace2, match="foobar\\s"): ... + | ^^^^^^^^^^^ RUF043 +41 | with pytest.raises(NonWhitespace2, match="foobar\\S"): ... +42 | with pytest.raises(WordCharacter2, match="foobar\\w"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:41:46: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +39 | with pytest.raises(NonDigit2, match="foobar\\D"): ... +40 | with pytest.raises(Whitespace2, match="foobar\\s"): ... +41 | with pytest.raises(NonWhitespace2, match="foobar\\S"): ... + | ^^^^^^^^^^^ RUF043 +42 | with pytest.raises(WordCharacter2, match="foobar\\w"): ... +43 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:42:46: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +40 | with pytest.raises(Whitespace2, match="foobar\\s"): ... +41 | with pytest.raises(NonWhitespace2, match="foobar\\S"): ... +42 | with pytest.raises(WordCharacter2, match="foobar\\w"): ... + | ^^^^^^^^^^^ RUF043 +43 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... +44 | with pytest.raises(EndOfInput2, match="foobar\\z"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:43:49: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +41 | with pytest.raises(NonWhitespace2, match="foobar\\S"): ... +42 | with pytest.raises(WordCharacter2, match="foobar\\w"): ... +43 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... + | ^^^^^^^^^^^ RUF043 +44 | with pytest.raises(EndOfInput2, match="foobar\\z"): ... + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:44:43: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +42 | with pytest.raises(WordCharacter2, match="foobar\\w"): ... +43 | with pytest.raises(NonWordCharacter2, match="foobar\\W"): ... +44 | with pytest.raises(EndOfInput2, match="foobar\\z"): ... + | ^^^^^^^^^^^ RUF043 + | + = help: Use a raw string or `re.escape()` to make the intention explicit + +RUF043.py:49:42: RUF043 Pattern passed to `match=` contains metacharacters but is neither escaped nor raw + | +47 | ### Acceptable false positives +48 | +49 | with pytest.raises(NameEscape, match="\\N{EN DASH}"): ... + | ^^^^^^^^^^^^^^ RUF043 + | + = help: Use a raw string or `re.escape()` to make the intention explicit diff --git a/ruff.schema.json b/ruff.schema.json index 6e95d80558dde..a55f658c9741e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3848,6 +3848,7 @@ "RUF04", "RUF040", "RUF041", + "RUF043", "RUF046", "RUF048", "RUF05",