diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP019.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP019.py index 64c616ede5..a89b6ee78a 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP019.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP019.py @@ -18,3 +18,20 @@ def print_third_word(word: Hello.Text) -> None: def print_fourth_word(word: Goodbye) -> None: print(word) + + +import typing_extensions +import typing_extensions as TypingExt +from typing_extensions import Text as TextAlias + + +def print_fifth_word(word: typing_extensions.Text) -> None: + print(word) + + +def print_sixth_word(word: TypingExt.Text) -> None: + print(word) + + +def print_seventh_word(word: TextAlias) -> None: + print(word) diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 37a577e8df..87895090e5 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -265,3 +265,7 @@ pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) -> pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool { settings.preview.is_enabled() } + +pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index bf8e7495f9..d882cd5cca 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -126,6 +126,7 @@ mod tests { } #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] + #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}__preview", path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 3f11b63579..2310880eed 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -1,15 +1,19 @@ use ruff_python_ast::Expr; +use std::fmt::{Display, Formatter}; use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::preview::is_typing_extensions_str_alias_enabled; use crate::{Edit, Fix, FixAvailability, Violation}; /// ## What it does /// Checks for uses of `typing.Text`. /// +/// In preview mode, also checks for `typing_extensions.Text`. +/// /// ## Why is this bad? /// `typing.Text` is an alias for `str`, and only exists for Python 2 /// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str` @@ -30,14 +34,16 @@ use crate::{Edit, Fix, FixAvailability, Violation}; /// ## References /// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text) #[derive(ViolationMetadata)] -pub(crate) struct TypingTextStrAlias; +pub(crate) struct TypingTextStrAlias { + module: TypingModule, +} impl Violation for TypingTextStrAlias { const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { - "`typing.Text` is deprecated, use `str`".to_string() + format!("`{}.Text` is deprecated, use `str`", self.module) } fn fix_title(&self) -> Option { @@ -47,16 +53,26 @@ impl Violation for TypingTextStrAlias { /// UP019 pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { - if !checker.semantic().seen_module(Modules::TYPING) { + if !checker + .semantic() + .seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS) + { return; } - if checker - .semantic() - .resolve_qualified_name(expr) - .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"])) - { - let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range()); + if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) { + let segments = qualified_name.segments(); + let module = match segments { + ["typing", "Text"] => TypingModule::Typing, + ["typing_extensions", "Text"] + if is_typing_extensions_str_alias_enabled(checker.settings()) => + { + TypingModule::TypingExtensions + } + _ => return, + }; + + let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias { module }, expr.range()); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( @@ -71,3 +87,18 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { }); } } + +#[derive(Copy, Clone, Debug)] +enum TypingModule { + Typing, + TypingExtensions, +} + +impl Display for TypingModule { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TypingModule::Typing => f.write_str("typing"), + TypingModule::TypingExtensions => f.write_str("typing_extensions"), + } + } +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap index d0af23c520..279648f87a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py.snap @@ -66,3 +66,5 @@ help: Replace with `str` - def print_fourth_word(word: Goodbye) -> None: 19 + def print_fourth_word(word: str) -> None: 20 | print(word) +21 | +22 | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap new file mode 100644 index 0000000000..89b257ccc2 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP019.py__preview.snap @@ -0,0 +1,119 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP019 [*] `typing.Text` is deprecated, use `str` + --> UP019.py:7:22 + | +7 | def print_word(word: Text) -> None: + | ^^^^ +8 | print(word) + | +help: Replace with `str` +4 | from typing import Text as Goodbye +5 | +6 | + - def print_word(word: Text) -> None: +7 + def print_word(word: str) -> None: +8 | print(word) +9 | +10 | + +UP019 [*] `typing.Text` is deprecated, use `str` + --> UP019.py:11:29 + | +11 | def print_second_word(word: typing.Text) -> None: + | ^^^^^^^^^^^ +12 | print(word) + | +help: Replace with `str` +8 | print(word) +9 | +10 | + - def print_second_word(word: typing.Text) -> None: +11 + def print_second_word(word: str) -> None: +12 | print(word) +13 | +14 | + +UP019 [*] `typing.Text` is deprecated, use `str` + --> UP019.py:15:28 + | +15 | def print_third_word(word: Hello.Text) -> None: + | ^^^^^^^^^^ +16 | print(word) + | +help: Replace with `str` +12 | print(word) +13 | +14 | + - def print_third_word(word: Hello.Text) -> None: +15 + def print_third_word(word: str) -> None: +16 | print(word) +17 | +18 | + +UP019 [*] `typing.Text` is deprecated, use `str` + --> UP019.py:19:29 + | +19 | def print_fourth_word(word: Goodbye) -> None: + | ^^^^^^^ +20 | print(word) + | +help: Replace with `str` +16 | print(word) +17 | +18 | + - def print_fourth_word(word: Goodbye) -> None: +19 + def print_fourth_word(word: str) -> None: +20 | print(word) +21 | +22 | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:28:28 + | +28 | def print_fifth_word(word: typing_extensions.Text) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^ +29 | print(word) + | +help: Replace with `str` +25 | from typing_extensions import Text as TextAlias +26 | +27 | + - def print_fifth_word(word: typing_extensions.Text) -> None: +28 + def print_fifth_word(word: str) -> None: +29 | print(word) +30 | +31 | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:32:28 + | +32 | def print_sixth_word(word: TypingExt.Text) -> None: + | ^^^^^^^^^^^^^^ +33 | print(word) + | +help: Replace with `str` +29 | print(word) +30 | +31 | + - def print_sixth_word(word: TypingExt.Text) -> None: +32 + def print_sixth_word(word: str) -> None: +33 | print(word) +34 | +35 | + +UP019 [*] `typing_extensions.Text` is deprecated, use `str` + --> UP019.py:36:30 + | +36 | def print_seventh_word(word: TextAlias) -> None: + | ^^^^^^^^^ +37 | print(word) + | +help: Replace with `str` +33 | print(word) +34 | +35 | + - def print_seventh_word(word: TextAlias) -> None: +36 + def print_seventh_word(word: str) -> None: +37 | print(word)