diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F722_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/pyflakes/F722.py rename to crates/ruff_linter/resources/test/fixtures/pyflakes/F722_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py new file mode 100644 index 0000000000000..203aa2b3ef269 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py @@ -0,0 +1,21 @@ +"""Regression test for #13824. + +Don't report an error when the function being annotated has the +`@no_type_check` decorator. + +However, we still want to ignore this annotation on classes. See +https://github.com/python/typing/pull/1615/files and the discussion on #14615. +""" + +from typing import no_type_check + + +@no_type_check +def f(arg: "this isn't python") -> "this isn't python either": + x: "this also isn't python" = 0 + + +@no_type_check +class C: + def f(arg: "this isn't python") -> "this isn't python either": + x: "this also isn't python" = 1 diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py new file mode 100644 index 0000000000000..d0c1b812b460e --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py @@ -0,0 +1,21 @@ +"""Regression test for #13824. + +Don't report an error when the function being annotated has the +`@no_type_check` decorator. + +However, we still want to ignore this annotation on classes. See +https://github.com/python/typing/pull/1615/files and the discussion on #14615. +""" + +import typing + + +@typing.no_type_check +def f(arg: "A") -> "R": + x: "A" = 1 + + +@typing.no_type_check +class C: + def f(self, arg: "B") -> "S": + x: "B" = 1 diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 1f02f66f3dccb..97154f4fccee5 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -723,6 +723,12 @@ impl<'a> Visitor<'a> for Checker<'a> { // Visit the decorators and arguments, but avoid the body, which will be // deferred. for decorator in decorator_list { + if self + .semantic + .match_typing_expr(&decorator.expression, "no_type_check") + { + self.semantic.flags |= SemanticModelFlags::NO_TYPE_CHECK; + } self.visit_decorator(decorator); } @@ -1851,6 +1857,9 @@ impl<'a> Checker<'a> { /// Visit an [`Expr`], and treat it as a type definition. fn visit_type_definition(&mut self, expr: &'a Expr) { + if self.semantic.in_no_type_check() { + return; + } let snapshot = self.semantic.flags; self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION; self.visit_expr(expr); diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index c01397206f2be..7fb0f32673a8d 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -94,7 +94,8 @@ mod tests { #[test_case(Rule::YieldOutsideFunction, Path::new("F704.py"))] #[test_case(Rule::ReturnOutsideFunction, Path::new("F706.py"))] #[test_case(Rule::DefaultExceptNotLast, Path::new("F707.py"))] - #[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722.py"))] + #[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722_0.py"))] + #[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722_1.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_0.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_2.py"))] @@ -159,6 +160,7 @@ mod tests { #[test_case(Rule::UndefinedName, Path::new("F821_26.pyi"))] #[test_case(Rule::UndefinedName, Path::new("F821_27.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_28.py"))] + #[test_case(Rule::UndefinedName, Path::new("F821_30.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_0.py.snap similarity index 63% rename from crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap rename to crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_0.py.snap index 3a41c4a06203c..0e45ac706c607 100644 --- a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722.py.snap +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_0.py.snap @@ -2,14 +2,14 @@ source: crates/ruff_linter/src/rules/pyflakes/mod.rs snapshot_kind: text --- -F722.py:9:12: F722 Syntax error in forward annotation: `///` +F722_0.py:9:12: F722 Syntax error in forward annotation: `///` | 9 | def g() -> "///": | ^^^^^ F722 10 | pass | -F722.py:13:4: F722 Syntax error in forward annotation: `List[int]☃` +F722_0.py:13:4: F722 Syntax error in forward annotation: `List[int]☃` | 13 | X: """List[int]"""'☃' = [] | ^^^^^^^^^^^^^^^^^^ F722 diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap new file mode 100644 index 0000000000000..f79147a188710 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F722_F722_1.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +snapshot_kind: text +--- +F722_1.py:20:16: F722 Syntax error in forward annotation: `this isn't python` + | +18 | @no_type_check +19 | class C: +20 | def f(arg: "this isn't python") -> "this isn't python either": + | ^^^^^^^^^^^^^^^^^^^ F722 +21 | x: "this also isn't python" = 1 + | + +F722_1.py:20:40: F722 Syntax error in forward annotation: `this isn't python either` + | +18 | @no_type_check +19 | class C: +20 | def f(arg: "this isn't python") -> "this isn't python either": + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ F722 +21 | x: "this also isn't python" = 1 + | + +F722_1.py:21:12: F722 Syntax error in forward annotation: `this also isn't python` + | +19 | class C: +20 | def f(arg: "this isn't python") -> "this isn't python either": +21 | x: "this also isn't python" = 1 + | ^^^^^^^^^^^^^^^^^^^^^^^^ F722 + | diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_30.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_30.py.snap new file mode 100644 index 0000000000000..65b4a6cb293c7 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_30.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_linter/src/rules/pyflakes/mod.rs +snapshot_kind: text +--- +F821_30.py:20:23: F821 Undefined name `B` + | +18 | @typing.no_type_check +19 | class C: +20 | def f(self, arg: "B") -> "S": + | ^ F821 +21 | x: "B" = 1 + | + +F821_30.py:20:31: F821 Undefined name `S` + | +18 | @typing.no_type_check +19 | class C: +20 | def f(self, arg: "B") -> "S": + | ^ F821 +21 | x: "B" = 1 + | + +F821_30.py:21:13: F821 Undefined name `B` + | +19 | class C: +20 | def f(self, arg: "B") -> "S": +21 | x: "B" = 1 + | ^ F821 + | diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 8f3235d7b0715..d2d4907881d81 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1584,6 +1584,11 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::ANNOTATION) } + /// Return `true` if the model is in a `@no_type_check` context. + pub const fn in_no_type_check(&self) -> bool { + self.flags.intersects(SemanticModelFlags::NO_TYPE_CHECK) + } + /// Return `true` if the model is in a typing-only type annotation. pub const fn in_typing_only_annotation(&self) -> bool { self.flags @@ -2222,6 +2227,23 @@ bitflags! { /// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring const ATTRIBUTE_DOCSTRING = 1 << 25; + /// The model is in a [no_type_check] context. + /// + /// This is used to skip type checking when the `@no_type_check` decorator is found. + /// + /// For example (adapted from [#13824]): + /// ```python + /// from typing import no_type_check + /// + /// @no_type_check + /// def fn(arg: "A") -> "R": + /// pass + /// ``` + /// + /// [no_type_check]: https://docs.python.org/3/library/typing.html#typing.no_type_check + /// [#13824]: https://github.com/astral-sh/ruff/issues/13824 + const NO_TYPE_CHECK = 1 << 26; + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();