Skip to content

Commit

Permalink
[pyflakes] Ignore errors in @no_type_check string annotations (`F…
Browse files Browse the repository at this point in the history
…722`, `F821`) (#15215)
  • Loading branch information
InSyncWithFoo authored Jan 3, 2025
1 parent 835b453 commit 6180f78
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F821_31.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
},
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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"))]
Expand Down Expand Up @@ -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"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---

Original file line number Diff line number Diff line change
@@ -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
|
Original file line number Diff line number Diff line change
@@ -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
|
22 changes: 22 additions & 0 deletions crates/ruff_python_semantic/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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();

Expand Down

0 comments on commit 6180f78

Please sign in to comment.