diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py b/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py index 79c9307695a91..10c8b4202f0fd 100644 Binary files a/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py and b/crates/ruff_linter/resources/test/fixtures/pylint/invalid_characters.py differ diff --git a/crates/ruff_linter/src/checkers/tokens.rs b/crates/ruff_linter/src/checkers/tokens.rs index cb0eecfad1d5f..dc157ba3e0ec7 100644 --- a/crates/ruff_linter/src/checkers/tokens.rs +++ b/crates/ruff_linter/src/checkers/tokens.rs @@ -8,7 +8,6 @@ use ruff_python_ast::PySourceType; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::Tokens; -use ruff_text_size::Ranged; use crate::directives::TodoComment; use crate::registry::{AsRule, Rule}; @@ -88,12 +87,7 @@ pub(crate) fn check_tokens( Rule::InvalidCharacterZeroWidthSpace, ]) { for token in tokens { - pylint::rules::invalid_string_characters( - &mut diagnostics, - token.kind(), - token.range(), - locator, - ); + pylint::rules::invalid_string_characters(&mut diagnostics, token, locator); } } diff --git a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs index efa6219a5d75b..7a30ee0763226 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/invalid_string_characters.rs @@ -1,9 +1,7 @@ -use ruff_diagnostics::AlwaysFixableViolation; -use ruff_diagnostics::Edit; -use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix}; +use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_parser::TokenKind; -use ruff_text_size::{TextLen, TextRange, TextSize}; +use ruff_python_parser::{Token, TokenKind}; +use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; @@ -29,14 +27,16 @@ use crate::Locator; #[derive(ViolationMetadata)] pub(crate) struct InvalidCharacterBackspace; -impl AlwaysFixableViolation for InvalidCharacterBackspace { +impl Violation for InvalidCharacterBackspace { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Invalid unescaped character backspace, use \"\\b\" instead".to_string() } - fn fix_title(&self) -> String { - "Replace with escape sequence".to_string() + fn fix_title(&self) -> Option { + Some("Replace with escape sequence".to_string()) } } @@ -62,14 +62,16 @@ impl AlwaysFixableViolation for InvalidCharacterBackspace { #[derive(ViolationMetadata)] pub(crate) struct InvalidCharacterSub; -impl AlwaysFixableViolation for InvalidCharacterSub { +impl Violation for InvalidCharacterSub { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Invalid unescaped character SUB, use \"\\x1A\" instead".to_string() } - fn fix_title(&self) -> String { - "Replace with escape sequence".to_string() + fn fix_title(&self) -> Option { + Some("Replace with escape sequence".to_string()) } } @@ -95,14 +97,16 @@ impl AlwaysFixableViolation for InvalidCharacterSub { #[derive(ViolationMetadata)] pub(crate) struct InvalidCharacterEsc; -impl AlwaysFixableViolation for InvalidCharacterEsc { +impl Violation for InvalidCharacterEsc { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Invalid unescaped character ESC, use \"\\x1B\" instead".to_string() } - fn fix_title(&self) -> String { - "Replace with escape sequence".to_string() + fn fix_title(&self) -> Option { + Some("Replace with escape sequence".to_string()) } } @@ -128,14 +132,16 @@ impl AlwaysFixableViolation for InvalidCharacterEsc { #[derive(ViolationMetadata)] pub(crate) struct InvalidCharacterNul; -impl AlwaysFixableViolation for InvalidCharacterNul { +impl Violation for InvalidCharacterNul { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Invalid unescaped character NUL, use \"\\0\" instead".to_string() } - fn fix_title(&self) -> String { - "Replace with escape sequence".to_string() + fn fix_title(&self) -> Option { + Some("Replace with escape sequence".to_string()) } } @@ -160,28 +166,29 @@ impl AlwaysFixableViolation for InvalidCharacterNul { #[derive(ViolationMetadata)] pub(crate) struct InvalidCharacterZeroWidthSpace; -impl AlwaysFixableViolation for InvalidCharacterZeroWidthSpace { +impl Violation for InvalidCharacterZeroWidthSpace { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Invalid unescaped character zero-width-space, use \"\\u200B\" instead".to_string() } - fn fix_title(&self) -> String { - "Replace with escape sequence".to_string() + fn fix_title(&self) -> Option { + Some("Replace with escape sequence".to_string()) } } /// PLE2510, PLE2512, PLE2513, PLE2514, PLE2515 pub(crate) fn invalid_string_characters( diagnostics: &mut Vec, - token: TokenKind, - range: TextRange, + token: &Token, locator: &Locator, ) { - let text = match token { + let text = match token.kind() { // We can't use the `value` field since it's decoded and e.g. for f-strings removed a curly // brace that escaped another curly brace, which would gives us wrong column information. - TokenKind::String | TokenKind::FStringMiddle => locator.slice(range), + TokenKind::String | TokenKind::FStringMiddle => locator.slice(token), _ => return, }; @@ -198,11 +205,16 @@ pub(crate) fn invalid_string_characters( } }; - let location = range.start() + TextSize::try_from(column).unwrap(); + let location = token.start() + TextSize::try_from(column).unwrap(); let range = TextRange::at(location, c.text_len()); - diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::safe_edit( - Edit::range_replacement(replacement.to_string(), range), - ))); + let mut diagnostic = Diagnostic::new(rule, range); + + if !token.unwrap_string_flags().is_raw_string() { + let edit = Edit::range_replacement(replacement.to_string(), range); + diagnostic.set_fix(Fix::safe_edit(edit)); + } + + diagnostics.push(diagnostic); } } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap index b6f94b3dbf93f..46b9cb6a3fcc5 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2510_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap index d90e38a14cc21..88630ccb7a5a9 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2512_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap index 72e7b93e0765f..eefd991446f13 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2513_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap index 49b80e52e968e..a06d6f645f090 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2514_invalid_characters.py.snap differ diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap index 6e29b04cba648..5b50a43e0589e 100644 Binary files a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap and b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE2515_invalid_characters.py.snap differ diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 436651fab6ba6..9574e4c23c47d 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -14,7 +14,7 @@ use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::str_prefix::{ AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, }; -use ruff_python_ast::{BoolOp, Int, IpyEscapeKind, Operator, StringFlags, UnaryOp}; +use ruff_python_ast::{AnyStringFlags, BoolOp, Int, IpyEscapeKind, Operator, StringFlags, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; #[derive(Clone, Copy, PartialEq, Eq)] @@ -50,8 +50,7 @@ impl Token { /// /// If it isn't a string or any f-string tokens. pub fn is_triple_quoted_string(self) -> bool { - assert!(self.is_any_string()); - self.flags.is_triple_quoted() + self.unwrap_string_flags().is_triple_quoted() } /// Returns the [`Quote`] style for the current string token of any kind. @@ -60,8 +59,26 @@ impl Token { /// /// If it isn't a string or any f-string tokens. pub fn string_quote_style(self) -> Quote { - assert!(self.is_any_string()); - self.flags.quote_style() + self.unwrap_string_flags().quote_style() + } + + /// Returns the [`AnyStringFlags`] style for the current string token of any kind. + /// + /// # Panics + /// + /// If it isn't a string or any f-string tokens. + pub fn unwrap_string_flags(self) -> AnyStringFlags { + self.string_flags() + .unwrap_or_else(|| panic!("token to be a string")) + } + + /// Returns true if the current token is a string and it is raw. + pub fn string_flags(self) -> Option { + if self.is_any_string() { + Some(self.flags.as_any_string_flags()) + } else { + None + } } /// Returns `true` if this is any kind of string token.