diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py index 32cc0ec4a0f7e5..baad09bf3f92a7 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP013.py @@ -46,3 +46,9 @@ X = TypedDict("X", { "some_config": int, # important }) + +# Private names should not be reported (OK) +WithPrivate = TypedDict("WithPrivate", {"__x": int}) + +# Dunder names should not be reported (OK) +WithDunder = TypedDict("WithDunder", {"__x__": int}) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs index 3891ae3ac7cbf4..09ace2c6273069 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/convert_typed_dict_functional_to_class.rs @@ -1,6 +1,5 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::helpers::is_dunder; use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt}; use ruff_python_codegen::Generator; use ruff_python_semantic::SemanticModel; @@ -15,12 +14,22 @@ use crate::checkers::ast::Checker; /// Checks for `TypedDict` declarations that use functional syntax. /// /// ## Why is this bad? -/// `TypedDict` subclasses can be defined either through a functional syntax +/// `TypedDict` types can be defined either through a functional syntax /// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`). /// /// The class syntax is more readable and generally preferred over the /// functional syntax. /// +/// Nonetheless, there are some situations in which it is impossible to use +/// the class-based syntax. This rule will not apply to those cases. Namely, +/// it is impossible to use the class-based syntax if any `TypedDict` fields are: +/// - Not valid [python identifiers] (for example, `@x`) +/// - [Python keywords] such as `in` +/// - [Private names] such as `__id` that would undergo [name mangling] at runtime +/// if the class-based syntax was used +/// - [Dunder names] such as `__int__` that can confuse type checkers if they're used +/// with the class-based syntax. +/// /// ## Example /// ```python /// from typing import TypedDict @@ -45,6 +54,12 @@ use crate::checkers::ast::Checker; /// /// ## References /// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict) +/// +/// [Private names]: https://docs.python.org/3/tutorial/classes.html#private-variables +/// [name mangling]: https://docs.python.org/3/reference/expressions.html#private-name-mangling +/// [python identifiers]: https://docs.python.org/3/reference/lexical_analysis.html#identifiers +/// [Python keywords]: https://docs.python.org/3/reference/lexical_analysis.html#keywords +/// [Dunder names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers #[derive(ViolationMetadata)] pub(crate) struct ConvertTypedDictFunctionalToClass { name: String, @@ -185,7 +200,10 @@ fn fields_from_dict_literal(items: &[ast::DictItem]) -> Option> { if !is_identifier(field.to_str()) { return None; } - if is_dunder(field.to_str()) { + // Converting TypedDict to class-based syntax is not safe if fields contain + // private or dunder names, because private names will be mangled and dunder + // names can confuse type checkers. + if field.to_str().starts_with("__") { return None; } Some(create_field_assignment_stmt(field.to_str(), value)) diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap index 09bb75d3d812b9..f428a3a4d7e82e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP013.py.snap @@ -264,6 +264,8 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax 47 | | "some_config": int, # important 48 | | }) | |__^ UP013 +49 | +50 | # Private names should not be reported (OK) | = help: Convert `X` to class syntax @@ -276,3 +278,6 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax 48 |-}) 46 |+class X(TypedDict): 47 |+ some_config: int +49 48 | +50 49 | # Private names should not be reported (OK) +51 50 | WithPrivate = TypedDict("WithPrivate", {"__x": int})