mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[pyflakes
] Ignore errors in @no_type_check
string annotations (F722
, F821
) (#15215)
This commit is contained in:
parent
835b453bfd
commit
6180f78da4
10 changed files with 173 additions and 1 deletions
8
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_32.py
vendored
Normal file
8
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_32.py
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
from datetime import datetime
|
||||
from typing import no_type_check
|
||||
|
||||
|
||||
# No errors
|
||||
|
||||
@no_type_check
|
||||
def f(a: datetime, b: "datetime"): ...
|
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
from typing import no_type_check
|
||||
|
||||
|
||||
# Errors
|
||||
|
||||
@no_type_check
|
||||
class C:
|
||||
def f(self, arg: "this isn't python") -> "this isn't python either":
|
||||
x: "this also isn't python" = 1
|
||||
|
||||
|
||||
# No errors
|
||||
|
||||
@no_type_check
|
||||
def f(arg: "this isn't python") -> "this isn't python either":
|
||||
x: "this also isn't python" = 0
|
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_31.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_31.py
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
import typing
|
||||
|
||||
|
||||
# Errors
|
||||
|
||||
@typing.no_type_check
|
||||
class C:
|
||||
def f(self, arg: "B") -> "S":
|
||||
x: "B" = 1
|
||||
|
||||
|
||||
# No errors
|
||||
|
||||
@typing.no_type_check
|
||||
def f(arg: "A") -> "R":
|
||||
x: "A" = 1
|
|
@ -24,6 +24,10 @@ pub(crate) fn unresolved_references(checker: &mut Checker) {
|
|||
}
|
||||
} else {
|
||||
if checker.enabled(Rule::UndefinedName) {
|
||||
if checker.semantic.in_no_type_check() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Avoid flagging if `NameError` is handled.
|
||||
if reference.exceptions().contains(Exceptions::NAME_ERROR) {
|
||||
continue;
|
||||
|
|
|
@ -449,6 +449,13 @@ impl<'a> Checker<'a> {
|
|||
match_fn(expr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Push `diagnostic` if the checker is not in a `@no_type_check` context.
|
||||
pub(crate) fn push_type_diagnostic(&mut self, diagnostic: Diagnostic) {
|
||||
if !self.semantic.in_no_type_check() {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for Checker<'a> {
|
||||
|
@ -724,6 +731,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
|||
// deferred.
|
||||
for decorator in decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
|
||||
if self
|
||||
.semantic
|
||||
.match_typing_expr(&decorator.expression, "no_type_check")
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::NO_TYPE_CHECK;
|
||||
}
|
||||
}
|
||||
|
||||
// Function annotations are always evaluated at runtime, unless future annotations
|
||||
|
@ -2348,8 +2362,10 @@ impl<'a> Checker<'a> {
|
|||
}
|
||||
self.parsed_type_annotation = None;
|
||||
} else {
|
||||
self.semantic.restore(snapshot);
|
||||
|
||||
if self.enabled(Rule::ForwardAnnotationSyntaxError) {
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
self.push_type_diagnostic(Diagnostic::new(
|
||||
pyflakes::rules::ForwardAnnotationSyntaxError {
|
||||
body: string_expr.value.to_string(),
|
||||
},
|
||||
|
|
|
@ -55,6 +55,7 @@ mod tests {
|
|||
#[test_case(Rule::UnusedImport, Path::new("F401_21.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_22.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_23.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_32.py"))]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.ipynb"))]
|
||||
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
|
||||
|
@ -95,6 +96,7 @@ mod tests {
|
|||
#[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_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"))]
|
||||
|
@ -160,6 +162,7 @@ mod tests {
|
|||
#[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::UndefinedName, Path::new("F821_31.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"))]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
F722_1.py:8:22: F722 Syntax error in forward annotation: `this isn't python`
|
||||
|
|
||||
6 | @no_type_check
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "this isn't python") -> "this isn't python either":
|
||||
| ^^^^^^^^^^^^^^^^^^^ F722
|
||||
9 | x: "this also isn't python" = 1
|
||||
|
|
||||
|
||||
F722_1.py:8:46: F722 Syntax error in forward annotation: `this isn't python either`
|
||||
|
|
||||
6 | @no_type_check
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "this isn't python") -> "this isn't python either":
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ F722
|
||||
9 | x: "this also isn't python" = 1
|
||||
|
|
||||
|
||||
F722_1.py:9:12: F722 Syntax error in forward annotation: `this also isn't python`
|
||||
|
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "this isn't python") -> "this isn't python either":
|
||||
9 | x: "this also isn't python" = 1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ F722
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
F821_31.py:8:23: F821 Undefined name `B`
|
||||
|
|
||||
6 | @typing.no_type_check
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "B") -> "S":
|
||||
| ^ F821
|
||||
9 | x: "B" = 1
|
||||
|
|
||||
|
||||
F821_31.py:8:31: F821 Undefined name `S`
|
||||
|
|
||||
6 | @typing.no_type_check
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "B") -> "S":
|
||||
| ^ F821
|
||||
9 | x: "B" = 1
|
||||
|
|
||||
|
||||
F821_31.py:9:13: F821 Undefined name `B`
|
||||
|
|
||||
7 | class C:
|
||||
8 | def f(self, arg: "B") -> "S":
|
||||
9 | x: "B" = 1
|
||||
| ^ F821
|
||||
|
|
||||
|
||||
F821_31.py:15:13: F821 Undefined name `A`
|
||||
|
|
||||
14 | @typing.no_type_check
|
||||
15 | def f(arg: "A") -> "R":
|
||||
| ^ F821
|
||||
16 | x: "A" = 1
|
||||
|
|
||||
|
||||
F821_31.py:15:21: F821 Undefined name `R`
|
||||
|
|
||||
14 | @typing.no_type_check
|
||||
15 | def f(arg: "A") -> "R":
|
||||
| ^ F821
|
||||
16 | x: "A" = 1
|
||||
|
|
||||
|
||||
F821_31.py:16:9: F821 Undefined name `A`
|
||||
|
|
||||
14 | @typing.no_type_check
|
||||
15 | def f(arg: "A") -> "R":
|
||||
16 | x: "A" = 1
|
||||
| ^ F821
|
||||
|
|
|
@ -1935,6 +1935,11 @@ impl<'a> SemanticModel<'a> {
|
|||
.intersects(SemanticModelFlags::ATTRIBUTE_DOCSTRING)
|
||||
}
|
||||
|
||||
/// 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 has traversed past the "top-of-file" import boundary.
|
||||
pub const fn seen_import_boundary(&self) -> bool {
|
||||
self.flags.intersects(SemanticModelFlags::IMPORT_BOUNDARY)
|
||||
|
@ -2477,6 +2482,23 @@ bitflags! {
|
|||
/// ```
|
||||
const ASSERT_STATEMENT = 1 << 29;
|
||||
|
||||
/// 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 << 30;
|
||||
|
||||
/// The context is in any type annotation.
|
||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue