Skip to content

Commit

Permalink
Differentiate between runtime and typing-time annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jul 7, 2023
1 parent 5908b39 commit a31e8db
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 27 deletions.
20 changes: 14 additions & 6 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,7 @@ where
{
if let Some(expr) = &arg_with_default.def.annotation {
if runtime_annotation {
self.visit_type_definition(expr);
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
};
Expand All @@ -1823,7 +1823,7 @@ where
if let Some(arg) = &args.vararg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_type_definition(expr);
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
};
Expand All @@ -1832,15 +1832,15 @@ where
if let Some(arg) = &args.kwarg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_type_definition(expr);
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
};
}
}
for expr in returns {
if runtime_annotation {
self.visit_type_definition(expr);
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
};
Expand Down Expand Up @@ -1992,7 +1992,7 @@ where
};

if runtime_annotation {
self.visit_type_definition(annotation);
self.visit_runtime_annotation(annotation);
} else {
self.visit_annotation(annotation);
}
Expand Down Expand Up @@ -2089,7 +2089,7 @@ where

fn visit_annotation(&mut self, expr: &'b Expr) {
let flags_snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::ANNOTATION;
self.semantic.flags |= SemanticModelFlags::TYPING_ANNOTATION;
self.visit_type_definition(expr);
self.semantic.flags = flags_snapshot;
}
Expand Down Expand Up @@ -4125,6 +4125,14 @@ impl<'a> Checker<'a> {
self.semantic.flags = snapshot;
}

/// Visit an [`Expr`], and treat it as a runtime-required type annotation.
fn visit_runtime_annotation(&mut self, expr: &'a Expr) {
let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::RUNTIME_ANNOTATION;
self.visit_type_definition(expr);
self.semantic.flags = snapshot;
}

/// Visit an [`Expr`], and treat it as a type definition.
fn visit_type_definition(&mut self, expr: &'a Expr) {
let snapshot = self.semantic.flags;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---
edge_case.py:5:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
5 | def main(_: List[int]) -> None:
| ^^^^ FA100
6 | a_list: t.List[str] = []
7 | a_list.append("hello")
|

edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but uses `typing.List`
|
5 | def main(_: List[int]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import a
3 | a_list.append("hello")
|

no_future_import_uses_lowercase.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str, int]) -> None:
| ^^^^^^^^^^^^^^ FA102
7 | del y
|


Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,18 @@ no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annot
3 | a_list.append("hello")
|

no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | del y
|

no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^ FA102
7 | del y
|


Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import
3 | a_list.append("hello")
|

no_future_import_uses_union_inner.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str | None, int]) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | z: tuple[str, str | None, str] = tuple(y)
8 | del z
|

no_future_import_uses_union_inner.py:6:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str | None, int]) -> None:
| ^^^^^^^^^^ FA102
7 | z: tuple[str, str | None, str] = tuple(y)
8 | del z
|

no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
6 | def hello(y: dict[str | None, int]) -> None:
Expand Down
74 changes: 53 additions & 21 deletions crates/ruff_python_semantic/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ impl<'a> SemanticModel<'a> {
/// Return the [`ExecutionContext`] of the current scope.
pub const fn execution_context(&self) -> ExecutionContext {
if self.in_type_checking_block()
|| self.in_annotation()
|| self.in_typing_annotation()
|| self.in_complex_string_type_definition()
|| self.in_simple_string_type_definition()
{
Expand Down Expand Up @@ -974,7 +974,17 @@ impl<'a> SemanticModel<'a> {

/// Return `true` if the context is in a type annotation.
pub const fn in_annotation(&self) -> bool {
self.flags.contains(SemanticModelFlags::ANNOTATION)
self.in_typing_annotation() || self.in_runtime_annotation()
}

/// Return `true` if the context is in a typing-only type annotation.
pub const fn in_typing_annotation(&self) -> bool {
self.flags.contains(SemanticModelFlags::TYPING_ANNOTATION)
}

/// Return `true` if the context is in a runtime-required type annotation.
pub const fn in_runtime_annotation(&self) -> bool {
self.flags.contains(SemanticModelFlags::RUNTIME_ANNOTATION)
}

/// Return `true` if the context is in a type definition.
Expand Down Expand Up @@ -1025,7 +1035,7 @@ impl<'a> SemanticModel<'a> {
pub const fn in_forward_reference(&self) -> bool {
self.in_simple_string_type_definition()
|| self.in_complex_string_type_definition()
|| (self.in_future_type_definition() && self.in_annotation())
|| (self.in_future_type_definition() && self.in_typing_annotation())
}

/// Return `true` if the context is in an exception handler.
Expand Down Expand Up @@ -1147,13 +1157,36 @@ bitflags! {
/// Flags indicating the current context of the analysis.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct SemanticModelFlags: u16 {
/// The context is in a type annotation.
/// The context is in a typing-time-only type annotation.
///
/// For example, the context could be visiting `int` in:
/// ```python
/// x: int = 1
/// def foo() -> int:
/// x: int = 1
/// ```
const ANNOTATION = 1 << 0;
///
/// In this case, Python doesn't require that the type annotation be evaluated at runtime.
///
/// Function argument annotations are always evaluated at runtime, unless
/// `from __future__ import annotations` is used. Annotated assignments are also evaluated
/// at runtime if they're within a module or class scope.
const TYPING_ANNOTATION = 1 << 0;

/// The context is in a runtime type annotation.
///
/// For example, the context could be visiting `int` in:
/// ```python
/// def foo(x: int) -> int:
/// ...
/// ```
///
/// In this case, Python requires that the type annotation be evaluated at runtime,
/// as it needs to be available on the function's `__annotations__` attribute.
///
/// Function argument annotations are always evaluated at runtime, unless
/// `from __future__ import annotations` is used. Annotated assignments are also evaluated
/// at runtime if they're within a module or class scope.
const RUNTIME_ANNOTATION = 1 << 1;

/// The context is in a type definition.
///
Expand All @@ -1167,7 +1200,7 @@ bitflags! {
/// All type annotations are also type definitions, but the converse is not true.
/// In our example, `int` is a type definition but not a type annotation, as it
/// doesn't appear in a type annotation context, but rather in a type definition.
const TYPE_DEFINITION = 1 << 1;
const TYPE_DEFINITION = 1 << 2;

/// The context is in a (deferred) "simple" string type definition.
///
Expand All @@ -1178,7 +1211,7 @@ bitflags! {
///
/// "Simple" string type definitions are those that consist of a single string literal,
/// as opposed to an implicitly concatenated string literal.
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 2;
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 3;

/// The context is in a (deferred) "complex" string type definition.
///
Expand All @@ -1189,7 +1222,7 @@ bitflags! {
///
/// "Complex" string type definitions are those that consist of a implicitly concatenated
/// string literals. These are uncommon but valid.
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 3;
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 4;

/// The context is in a (deferred) `__future__` type definition.
///
Expand All @@ -1202,7 +1235,7 @@ bitflags! {
///
/// `__future__`-style type annotations are only enabled if the `annotations` feature
/// is enabled via `from __future__ import annotations`.
const FUTURE_TYPE_DEFINITION = 1 << 4;
const FUTURE_TYPE_DEFINITION = 1 << 5;

/// The context is in an exception handler.
///
Expand All @@ -1213,23 +1246,23 @@ bitflags! {
/// except Exception:
/// x: int = 1
/// ```
const EXCEPTION_HANDLER = 1 << 5;
const EXCEPTION_HANDLER = 1 << 6;

/// The context is in an f-string.
///
/// For example, the context could be visiting `x` in:
/// ```python
/// f'{x}'
/// ```
const F_STRING = 1 << 6;
const F_STRING = 1 << 7;

/// The context is in a nested f-string.
///
/// For example, the context could be visiting `x` in:
/// ```python
/// f'{f"{x}"}'
/// ```
const NESTED_F_STRING = 1 << 7;
const NESTED_F_STRING = 1 << 8;

/// The context is in a boolean test.
///
Expand All @@ -1241,7 +1274,7 @@ bitflags! {
///
/// The implication is that the actual value returned by the current expression is
/// not used, only its truthiness.
const BOOLEAN_TEST = 1 << 8;
const BOOLEAN_TEST = 1 << 9;

/// The context is in a `typing::Literal` annotation.
///
Expand All @@ -1250,15 +1283,15 @@ bitflags! {
/// def f(x: Literal["A", "B", "C"]):
/// ...
/// ```
const LITERAL = 1 << 9;
const LITERAL = 1 << 10;

/// The context is in a subscript expression.
///
/// For example, the context could be visiting `x["a"]` in:
/// ```python
/// x["a"]["b"]
/// ```
const SUBSCRIPT = 1 << 10;
const SUBSCRIPT = 1 << 11;

/// The context is in a type-checking block.
///
Expand All @@ -1270,8 +1303,7 @@ bitflags! {
/// if TYPE_CHECKING:
/// x: int = 1
/// ```
const TYPE_CHECKING_BLOCK = 1 << 11;

const TYPE_CHECKING_BLOCK = 1 << 12;

/// The context has traversed past the "top-of-file" import boundary.
///
Expand All @@ -1284,7 +1316,7 @@ bitflags! {
///
/// x: int = 1
/// ```
const IMPORT_BOUNDARY = 1 << 12;
const IMPORT_BOUNDARY = 1 << 13;

/// The context has traversed past the `__future__` import boundary.
///
Expand All @@ -1299,7 +1331,7 @@ bitflags! {
///
/// Python considers it a syntax error to import from `__future__` after
/// any other non-`__future__`-importing statements.
const FUTURES_BOUNDARY = 1 << 13;
const FUTURES_BOUNDARY = 1 << 14;

/// `__future__`-style type annotations are enabled in this context.
///
Expand All @@ -1311,7 +1343,7 @@ bitflags! {
/// def f(x: int) -> int:
/// ...
/// ```
const FUTURE_ANNOTATIONS = 1 << 14;
const FUTURE_ANNOTATIONS = 1 << 15;
}
}

Expand Down

0 comments on commit a31e8db

Please sign in to comment.