diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi index b25a02db20c7b..bdfcb00c53418 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -1,6 +1,7 @@ import warnings import typing_extensions from typing_extensions import deprecated +from typing import Literal,Annotated,TypeAlias def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK def f2( @@ -67,5 +68,13 @@ def not_a_deprecated_function() -> None: ... fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053 -# see https://github.com/astral-sh/ruff/issues/12995 -def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):... \ No newline at end of file +# See https://github.com/astral-sh/ruff/issues/12995 +def foo(bar: Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):... #OK + +def foo(bar: Annotated[int,"aaaaaaaloooooooooooooongannnnnoooottttatiooooooooooooooon"]):... #OK + +x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooO"] #OK +y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] #OK + +def f(x:int) -> "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb":... #OK + diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 814f2d9f38fab..678edcbacb46d 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1422,6 +1422,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } // Ex) Annotated[int, "Hello, world!"] Some(typing::SubscriptKind::PEP593Annotation) => { + self.semantic.flags |= SemanticModelFlags::TYPING_ANNOTATED_PEP593; // First argument is a type (including forward references); the // rest are arbitrary Python objects. if let Expr::Tuple(ast::ExprTuple { diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 1d7a85b2470e2..8ae1f55d9b928 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -59,7 +59,16 @@ pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike return; } - if semantic.in_annotation() { + // Ignore strings in typing annotations like `Literal["YouAreHere"]` or `Annotated[int, "YouAreHere"]` + // + // This does not ignore other instances of strings in annotations, such as return types + // for long classes wrapped in quotes: + // ```python + // def f(x:int) -> "StillChecksThisString" + // ``` + // If it becomes desirable to skip annotations more broadly, replace the + // below with `if semantic.in_annotation()`. + if semantic.in_typing_literal() | semantic.in_typing_annotated_pep593() { return; } diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap index 501c5a310067b..d21d8804ba6d0 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI053_PYI053.pyi.snap @@ -1,168 +1,168 @@ --- source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs --- -PYI053.pyi:7:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted - | -5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK -6 | def f2( -7 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -8 | ) -> None: ... -9 | def f3( - | - = help: Replace with `...` +PYI053.pyi:8:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted + | + 6 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK + 7 | def f2( + 8 | x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 + 9 | ) -> None: ... +10 | def f3( + | + = help: Replace with `...` ℹ Safe fix -4 4 | -5 5 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK -6 6 | def f2( -7 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053 - 7 |+ x: str = ..., # Error: PYI053 -8 8 | ) -> None: ... -9 9 | def f3( -10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK +5 5 | +6 6 | def f1(x: str = "50 character stringggggggggggggggggggggggggggggggg") -> None: ... # OK +7 7 | def f2( +8 |- x: str = "51 character stringgggggggggggggggggggggggggggggggg", # Error: PYI053 + 8 |+ x: str = ..., # Error: PYI053 +9 9 | ) -> None: ... +10 10 | def f3( +11 11 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK -PYI053.pyi:13:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:14:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -11 | ) -> None: ... -12 | def f4( -13 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053 +12 | ) -> None: ... +13 | def f4( +14 | x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -14 | ) -> None: ... -15 | def f5( +15 | ) -> None: ... +16 | def f5( | = help: Replace with `...` ℹ Safe fix -10 10 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK -11 11 | ) -> None: ... -12 12 | def f4( -13 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053 - 13 |+ x: str = ..., # Error: PYI053 -14 14 | ) -> None: ... -15 15 | def f5( -16 16 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK +11 11 | x: str = "50 character stringgggggggggggggggggggggggggggggg\U0001f600", # OK +12 12 | ) -> None: ... +13 13 | def f4( +14 |- x: str = "51 character stringggggggggggggggggggggggggggggggg\U0001f600", # Error: PYI053 + 14 |+ x: str = ..., # Error: PYI053 +15 15 | ) -> None: ... +16 16 | def f5( +17 17 | x: bytes = b"50 character byte stringgggggggggggggggggggggggggg", # OK -PYI053.pyi:25:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:26:16: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -23 | ) -> None: ... -24 | def f8( -25 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053 +24 | ) -> None: ... +25 | def f8( +26 | x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -26 | ) -> None: ... +27 | ) -> None: ... | = help: Replace with `...` ℹ Safe fix -22 22 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK -23 23 | ) -> None: ... -24 24 | def f8( -25 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053 - 25 |+ x: bytes = ..., # Error: PYI053 -26 26 | ) -> None: ... -27 27 | -28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK +23 23 | x: bytes = b"50 character byte stringggggggggggggggggggggggggg\xff", # OK +24 24 | ) -> None: ... +25 25 | def f8( +26 |- x: bytes = b"51 character byte stringgggggggggggggggggggggggggg\xff", # Error: PYI053 + 26 |+ x: bytes = ..., # Error: PYI053 +27 27 | ) -> None: ... +28 28 | +29 29 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK -PYI053.pyi:30:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:31:12: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK -29 | -30 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 +29 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK +30 | +31 | bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -31 | -32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK +32 | +33 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK | = help: Replace with `...` ℹ Safe fix -27 27 | -28 28 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK -29 29 | -30 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 - 30 |+bar: str = ... # Error: PYI053 -31 31 | -32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK -33 33 | +28 28 | +29 29 | foo: str = "50 character stringggggggggggggggggggggggggggggggg" # OK +30 30 | +31 |-bar: str = "51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 + 31 |+bar: str = ... # Error: PYI053 +32 32 | +33 33 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK +34 34 | -PYI053.pyi:34:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:35:14: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK -33 | -34 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 +33 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK +34 | +35 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -35 | -36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK +36 | +37 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK | = help: Replace with `...` ℹ Safe fix -31 31 | -32 32 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK -33 33 | -34 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 - 34 |+qux: bytes = ... # Error: PYI053 -35 35 | -36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK -37 37 | +32 32 | +33 33 | baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK +34 34 | +35 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053 + 35 |+qux: bytes = ... # Error: PYI053 +36 36 | +37 37 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK +38 38 | -PYI053.pyi:38:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:39:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK -37 | -38 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 +37 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK +38 | +39 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -39 | -40 | class Demo: +40 | +41 | class Demo: | = help: Replace with `...` ℹ Safe fix -35 35 | -36 36 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK -37 37 | -38 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 - 38 |+fbar: str = ... # Error: PYI053 -39 39 | -40 40 | class Demo: -41 41 | """Docstrings are excluded from this rule. Some padding.""" # OK +36 36 | +37 37 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK +38 38 | +39 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 + 39 |+fbar: str = ... # Error: PYI053 +40 40 | +41 41 | class Demo: +42 42 | """Docstrings are excluded from this rule. Some padding.""" # OK -PYI053.pyi:64:5: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:65:5: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -63 | @not_warnings_dot_deprecated( -64 | "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053 +64 | @not_warnings_dot_deprecated( +65 | "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -65 | ) -66 | def not_a_deprecated_function() -> None: ... +66 | ) +67 | def not_a_deprecated_function() -> None: ... | = help: Replace with `...` ℹ Safe fix -61 61 | ) -> Callable[[Callable[[], None]], Callable[[], None]]: ... -62 62 | -63 63 | @not_warnings_dot_deprecated( -64 |- "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053 - 64 |+ ... # Error: PYI053 -65 65 | ) -66 66 | def not_a_deprecated_function() -> None: ... -67 67 | +62 62 | ) -> Callable[[Callable[[], None]], Callable[[], None]]: ... +63 63 | +64 64 | @not_warnings_dot_deprecated( +65 |- "Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!" # Error: PYI053 + 65 |+ ... # Error: PYI053 +66 66 | ) +67 67 | def not_a_deprecated_function() -> None: ... +68 68 | -PYI053.pyi:68:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted +PYI053.pyi:69:13: PYI053 [*] String and bytes literals longer than 50 characters are not permitted | -66 | def not_a_deprecated_function() -> None: ... -67 | -68 | fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053 +67 | def not_a_deprecated_function() -> None: ... +68 | +69 | fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053 -69 | -70 | # see https://github.com/astral-sh/ruff/issues/12995 +70 | +71 | # See https://github.com/astral-sh/ruff/issues/12995 | = help: Replace with `...` ℹ Safe fix -65 65 | ) -66 66 | def not_a_deprecated_function() -> None: ... -67 67 | -68 |-fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053 - 68 |+fbaz: str = ... # Error: PYI053 -69 69 | -70 70 | # see https://github.com/astral-sh/ruff/issues/12995 -71 71 | def foo(bar: typing.Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):... +66 66 | ) +67 67 | def not_a_deprecated_function() -> None: ... +68 68 | +69 |-fbaz: str = f"51 character {foo} stringgggggggggggggggggggggggggg" # Error: PYI053 + 69 |+fbaz: str = ... # Error: PYI053 +70 70 | +71 71 | # See https://github.com/astral-sh/ruff/issues/12995 +72 72 | def foo(bar: Literal["a", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"]):... #OK diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index b7377e83f4629..72c8527b2e95b 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1682,11 +1682,17 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::BOOLEAN_TEST) } - /// Return `true` if the model is in a `typing::Literal` annotation. + /// Return `true` if the model is in a `typing.Literal` annotation. pub const fn in_typing_literal(&self) -> bool { self.flags.intersects(SemanticModelFlags::TYPING_LITERAL) } + /// Return `true` if the model is in a `typing.Annotated` annotation. + pub const fn in_typing_annotated_pep593(&self) -> bool { + self.flags + .intersects(SemanticModelFlags::TYPING_ANNOTATED_PEP593) + } + /// Return `true` if the model is in a subscript expression. pub const fn in_subscript(&self) -> bool { self.flags.intersects(SemanticModelFlags::SUBSCRIPT) @@ -2019,7 +2025,7 @@ bitflags! { /// not used, only its truthiness. const BOOLEAN_TEST = 1 << 9; - /// The model is in a `typing::Literal` annotation. + /// The model is in a `typing.Literal` annotation. /// /// For example, the model could be visiting any of `"A"`, `"B"`, or `"C"` in: /// ```python @@ -2229,6 +2235,16 @@ bitflags! { /// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring const ATTRIBUTE_DOCSTRING = 1 << 26; + /// The model is in a `typing.Annotated` annotation. + /// + /// For example, the model could be visiting `int` or "A" in + /// ```python + /// def f(x: Annotated[int, "A"]): ... + /// ``` + /// + /// [PEP 593]: https://peps.python.org/pep-0593/ + const TYPING_ANNOTATED_PEP593 = 1<<27; + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();