[pyflakes] Avoid false positives in @no_type_check contexts (F821, F722) (#14615)

This commit is contained in:
Brent Westbrook 2024-11-26 14:13:43 -05:00 committed by GitHub
parent b94d6cf567
commit 9f446faa6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 136 additions and 3 deletions

View file

@ -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

View file

@ -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

View file

@ -723,6 +723,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Visit the decorators and arguments, but avoid the body, which will be // Visit the decorators and arguments, but avoid the body, which will be
// deferred. // deferred.
for decorator in decorator_list { 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); self.visit_decorator(decorator);
} }
@ -1851,6 +1857,9 @@ impl<'a> Checker<'a> {
/// Visit an [`Expr`], and treat it as a type definition. /// Visit an [`Expr`], and treat it as a type definition.
fn visit_type_definition(&mut self, expr: &'a Expr) { fn visit_type_definition(&mut self, expr: &'a Expr) {
if self.semantic.in_no_type_check() {
return;
}
let snapshot = self.semantic.flags; let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION; self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(expr); self.visit_expr(expr);

View file

@ -94,7 +94,8 @@ mod tests {
#[test_case(Rule::YieldOutsideFunction, Path::new("F704.py"))] #[test_case(Rule::YieldOutsideFunction, Path::new("F704.py"))]
#[test_case(Rule::ReturnOutsideFunction, Path::new("F706.py"))] #[test_case(Rule::ReturnOutsideFunction, Path::new("F706.py"))]
#[test_case(Rule::DefaultExceptNotLast, Path::new("F707.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_0.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))] #[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_2.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_26.pyi"))]
#[test_case(Rule::UndefinedName, Path::new("F821_27.py"))] #[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_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.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]

View file

@ -2,14 +2,14 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text 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() -> "///": 9 | def g() -> "///":
| ^^^^^ F722 | ^^^^^ F722
10 | pass 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]"""'☃' = [] 13 | X: """List[int]"""'☃' = []
| ^^^^^^^^^^^^^^^^^^ F722 | ^^^^^^^^^^^^^^^^^^ F722

View file

@ -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
|

View file

@ -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
|

View file

@ -1584,6 +1584,11 @@ impl<'a> SemanticModel<'a> {
self.flags.intersects(SemanticModelFlags::ANNOTATION) 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. /// Return `true` if the model is in a typing-only type annotation.
pub const fn in_typing_only_annotation(&self) -> bool { pub const fn in_typing_only_annotation(&self) -> bool {
self.flags self.flags
@ -2222,6 +2227,23 @@ bitflags! {
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring /// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const ATTRIBUTE_DOCSTRING = 1 << 25; 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. /// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();