mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-18 17:40:37 +00:00
Allow flake8-type-checking
rules to automatically quote runtime-evaluated references (#6001)
## Summary This allows us to fix usages like: ```python from pandas import DataFrame def baz() -> DataFrame: ... ``` By quoting the `DataFrame` in `-> DataFrame`. Without quotes, moving `from pandas import DataFrame` into an `if TYPE_CHECKING:` block will fail at runtime, since Python tries to evaluate the annotation to add it to the function's `__annotations__`. Unfortunately, this does require us to split our "annotation kind" flags into three categories, rather than two: - `typing-only`: The annotation is only evaluated at type-checking-time. - `runtime-evaluated`: Python will evaluate the annotation at runtime (like above) -- but we're willing to quote it. - `runtime-required`: Python will evaluate the annotation at runtime (like above), and some library (like Pydantic) needs it to be available at runtime, so we _can't_ quote it. This functionality is gated behind a setting (`flake8-type-checking.quote-annotations`). Closes https://github.com/astral-sh/ruff/issues/5559.
This commit is contained in:
parent
4d2ee5bf98
commit
1a65e544c5
18 changed files with 1034 additions and 208 deletions
|
@ -291,9 +291,12 @@ impl<'a> SemanticModel<'a> {
|
|||
if let Some(binding_id) = self.scopes.global().get(name.id.as_str()) {
|
||||
if !self.bindings[binding_id].is_unbound() {
|
||||
// Mark the binding as used.
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(ScopeId::global(), name.range, self.flags);
|
||||
let reference_id = self.resolved_references.push(
|
||||
ScopeId::global(),
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
// Mark any submodule aliases as used.
|
||||
|
@ -302,6 +305,7 @@ impl<'a> SemanticModel<'a> {
|
|||
{
|
||||
let reference_id = self.resolved_references.push(
|
||||
ScopeId::global(),
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
|
@ -356,18 +360,24 @@ impl<'a> SemanticModel<'a> {
|
|||
|
||||
if let Some(binding_id) = scope.get(name.id.as_str()) {
|
||||
// Mark the binding as used.
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(self.scope_id, name.range, self.flags);
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
// Mark any submodule aliases as used.
|
||||
if let Some(binding_id) =
|
||||
self.resolve_submodule(name.id.as_str(), scope_id, binding_id)
|
||||
{
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(self.scope_id, name.range, self.flags);
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
|
||||
|
@ -431,9 +441,12 @@ impl<'a> SemanticModel<'a> {
|
|||
// The `x` in `print(x)` should resolve to the `x` in `x = 1`.
|
||||
BindingKind::UnboundException(Some(binding_id)) => {
|
||||
// Mark the binding as used.
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(self.scope_id, name.range, self.flags);
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
|
||||
// Mark any submodule aliases as used.
|
||||
|
@ -442,6 +455,7 @@ impl<'a> SemanticModel<'a> {
|
|||
{
|
||||
let reference_id = self.resolved_references.push(
|
||||
self.scope_id,
|
||||
self.node_id,
|
||||
name.range,
|
||||
self.flags,
|
||||
);
|
||||
|
@ -979,6 +993,23 @@ impl<'a> SemanticModel<'a> {
|
|||
&self.nodes[node_id]
|
||||
}
|
||||
|
||||
/// Given a [`Expr`], return its parent, if any.
|
||||
#[inline]
|
||||
pub fn parent_expression(&self, node_id: NodeId) -> Option<&'a Expr> {
|
||||
self.nodes
|
||||
.ancestor_ids(node_id)
|
||||
.filter_map(|id| self.nodes[id].as_expression())
|
||||
.nth(1)
|
||||
}
|
||||
|
||||
/// Given a [`NodeId`], return the [`NodeId`] of the parent expression, if any.
|
||||
pub fn parent_expression_id(&self, node_id: NodeId) -> Option<NodeId> {
|
||||
self.nodes
|
||||
.ancestor_ids(node_id)
|
||||
.filter(|id| self.nodes[*id].is_expression())
|
||||
.nth(1)
|
||||
}
|
||||
|
||||
/// Return the [`Stmt`] corresponding to the given [`NodeId`].
|
||||
#[inline]
|
||||
pub fn statement(&self, node_id: NodeId) -> &'a Stmt {
|
||||
|
@ -1007,11 +1038,10 @@ impl<'a> SemanticModel<'a> {
|
|||
|
||||
/// Return the [`Expr`] corresponding to the given [`NodeId`].
|
||||
#[inline]
|
||||
pub fn expression(&self, node_id: NodeId) -> &'a Expr {
|
||||
pub fn expression(&self, node_id: NodeId) -> Option<&'a Expr> {
|
||||
self.nodes
|
||||
.ancestor_ids(node_id)
|
||||
.find_map(|id| self.nodes[id].as_expression())
|
||||
.expect("No expression found")
|
||||
}
|
||||
|
||||
/// Returns an [`Iterator`] over the expressions, starting from the given [`NodeId`].
|
||||
|
@ -1186,17 +1216,17 @@ impl<'a> SemanticModel<'a> {
|
|||
|
||||
/// Add a reference to the given [`BindingId`] in the local scope.
|
||||
pub fn add_local_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||
let reference_id = self
|
||||
.resolved_references
|
||||
.push(self.scope_id, range, self.flags);
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(self.scope_id, self.node_id, range, self.flags);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
|
||||
/// Add a reference to the given [`BindingId`] in the global scope.
|
||||
pub fn add_global_reference(&mut self, binding_id: BindingId, range: TextRange) {
|
||||
let reference_id = self
|
||||
.resolved_references
|
||||
.push(ScopeId::global(), range, self.flags);
|
||||
let reference_id =
|
||||
self.resolved_references
|
||||
.push(ScopeId::global(), self.node_id, range, self.flags);
|
||||
self.bindings[binding_id].references.push(reference_id);
|
||||
}
|
||||
|
||||
|
@ -1299,10 +1329,16 @@ impl<'a> SemanticModel<'a> {
|
|||
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
||||
}
|
||||
|
||||
/// Return `true` if the model is in a runtime-required type annotation.
|
||||
pub const fn in_runtime_annotation(&self) -> bool {
|
||||
/// Return `true` if the context is in a runtime-evaluated type annotation.
|
||||
pub const fn in_runtime_evaluated_annotation(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::RUNTIME_ANNOTATION)
|
||||
.intersects(SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a runtime-required type annotation.
|
||||
pub const fn in_runtime_required_annotation(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::RUNTIME_REQUIRED_ANNOTATION)
|
||||
}
|
||||
|
||||
/// Return `true` if the model is in a type definition.
|
||||
|
@ -1474,8 +1510,9 @@ impl ShadowedBinding {
|
|||
bitflags! {
|
||||
/// Flags indicating the current model state.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct SemanticModelFlags: u16 {
|
||||
/// The model is in a typing-time-only type annotation.
|
||||
pub struct SemanticModelFlags: u32 {
|
||||
/// The model is in a type annotation that will only be evaluated when running a type
|
||||
/// checker.
|
||||
///
|
||||
/// For example, the model could be visiting `int` in:
|
||||
/// ```python
|
||||
|
@ -1490,7 +1527,7 @@ bitflags! {
|
|||
/// are any annotated assignments in module or class scopes.
|
||||
const TYPING_ONLY_ANNOTATION = 1 << 0;
|
||||
|
||||
/// The model is in a runtime type annotation.
|
||||
/// The model is in a type annotation that will be evaluated at runtime.
|
||||
///
|
||||
/// For example, the model could be visiting `int` in:
|
||||
/// ```python
|
||||
|
@ -1504,7 +1541,27 @@ bitflags! {
|
|||
/// If `from __future__ import annotations` is used, all annotations are evaluated at
|
||||
/// typing time. Otherwise, all function argument annotations are evaluated at runtime, as
|
||||
/// are any annotated assignments in module or class scopes.
|
||||
const RUNTIME_ANNOTATION = 1 << 1;
|
||||
const RUNTIME_EVALUATED_ANNOTATION = 1 << 1;
|
||||
|
||||
/// The model is in a type annotation that is _required_ to be available at runtime.
|
||||
///
|
||||
/// For example, the context could be visiting `int` in:
|
||||
/// ```python
|
||||
/// from pydantic import BaseModel
|
||||
///
|
||||
/// class Foo(BaseModel):
|
||||
/// x: int
|
||||
/// ```
|
||||
///
|
||||
/// In this case, Pydantic requires that the type annotation be available at runtime
|
||||
/// in order to perform runtime type-checking.
|
||||
///
|
||||
/// Unlike [`RUNTIME_EVALUATED_ANNOTATION`], annotations that are marked as
|
||||
/// [`RUNTIME_REQUIRED_ANNOTATION`] cannot be deferred to typing time via conversion to a
|
||||
/// forward reference (e.g., by wrapping the type in quotes), as the annotations are not
|
||||
/// only required by the Python interpreter, but by runtime type checkers too.
|
||||
const RUNTIME_REQUIRED_ANNOTATION = 1 << 2;
|
||||
|
||||
|
||||
/// The model is in a type definition.
|
||||
///
|
||||
|
@ -1518,7 +1575,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 << 2;
|
||||
const TYPE_DEFINITION = 1 << 3;
|
||||
|
||||
/// The model is in a (deferred) "simple" string type definition.
|
||||
///
|
||||
|
@ -1529,7 +1586,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 << 3;
|
||||
const SIMPLE_STRING_TYPE_DEFINITION = 1 << 4;
|
||||
|
||||
/// The model is in a (deferred) "complex" string type definition.
|
||||
///
|
||||
|
@ -1540,7 +1597,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 << 4;
|
||||
const COMPLEX_STRING_TYPE_DEFINITION = 1 << 5;
|
||||
|
||||
/// The model is in a (deferred) `__future__` type definition.
|
||||
///
|
||||
|
@ -1553,7 +1610,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 << 5;
|
||||
const FUTURE_TYPE_DEFINITION = 1 << 6;
|
||||
|
||||
/// The model is in an exception handler.
|
||||
///
|
||||
|
@ -1564,7 +1621,7 @@ bitflags! {
|
|||
/// except Exception:
|
||||
/// x: int = 1
|
||||
/// ```
|
||||
const EXCEPTION_HANDLER = 1 << 6;
|
||||
const EXCEPTION_HANDLER = 1 << 7;
|
||||
|
||||
/// The model is in an f-string.
|
||||
///
|
||||
|
@ -1572,7 +1629,7 @@ bitflags! {
|
|||
/// ```python
|
||||
/// f'{x}'
|
||||
/// ```
|
||||
const F_STRING = 1 << 7;
|
||||
const F_STRING = 1 << 8;
|
||||
|
||||
/// The model is in a boolean test.
|
||||
///
|
||||
|
@ -1584,7 +1641,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 model is in a `typing::Literal` annotation.
|
||||
///
|
||||
|
@ -1593,7 +1650,7 @@ bitflags! {
|
|||
/// def f(x: Literal["A", "B", "C"]):
|
||||
/// ...
|
||||
/// ```
|
||||
const TYPING_LITERAL = 1 << 9;
|
||||
const TYPING_LITERAL = 1 << 10;
|
||||
|
||||
/// The model is in a subscript expression.
|
||||
///
|
||||
|
@ -1601,7 +1658,7 @@ bitflags! {
|
|||
/// ```python
|
||||
/// x["a"]["b"]
|
||||
/// ```
|
||||
const SUBSCRIPT = 1 << 10;
|
||||
const SUBSCRIPT = 1 << 11;
|
||||
|
||||
/// The model is in a type-checking block.
|
||||
///
|
||||
|
@ -1613,7 +1670,7 @@ bitflags! {
|
|||
/// if TYPE_CHECKING:
|
||||
/// x: int = 1
|
||||
/// ```
|
||||
const TYPE_CHECKING_BLOCK = 1 << 11;
|
||||
const TYPE_CHECKING_BLOCK = 1 << 12;
|
||||
|
||||
/// The model has traversed past the "top-of-file" import boundary.
|
||||
///
|
||||
|
@ -1626,7 +1683,7 @@ bitflags! {
|
|||
///
|
||||
/// x: int = 1
|
||||
/// ```
|
||||
const IMPORT_BOUNDARY = 1 << 12;
|
||||
const IMPORT_BOUNDARY = 1 << 13;
|
||||
|
||||
/// The model has traversed past the `__future__` import boundary.
|
||||
///
|
||||
|
@ -1641,7 +1698,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 model.
|
||||
///
|
||||
|
@ -1653,7 +1710,7 @@ bitflags! {
|
|||
/// def f(x: int) -> int:
|
||||
/// ...
|
||||
/// ```
|
||||
const FUTURE_ANNOTATIONS = 1 << 14;
|
||||
const FUTURE_ANNOTATIONS = 1 << 15;
|
||||
|
||||
/// The model is in a type parameter definition.
|
||||
///
|
||||
|
@ -1663,10 +1720,11 @@ bitflags! {
|
|||
///
|
||||
/// Record = TypeVar("Record")
|
||||
///
|
||||
const TYPE_PARAM_DEFINITION = 1 << 15;
|
||||
const TYPE_PARAM_DEFINITION = 1 << 16;
|
||||
|
||||
/// The context is in any type annotation.
|
||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_ANNOTATION.bits();
|
||||
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();
|
||||
|
||||
|
||||
/// The context is in any string type definition.
|
||||
const STRING_TYPE_DEFINITION = Self::SIMPLE_STRING_TYPE_DEFINITION.bits()
|
||||
|
|
|
@ -8,11 +8,14 @@ use ruff_text_size::{Ranged, TextRange};
|
|||
|
||||
use crate::context::ExecutionContext;
|
||||
use crate::scope::ScopeId;
|
||||
use crate::{Exceptions, SemanticModelFlags};
|
||||
use crate::{Exceptions, NodeId, SemanticModelFlags};
|
||||
|
||||
/// A resolved read reference to a name in a program.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedReference {
|
||||
/// The expression that the reference occurs in. `None` if the reference is a global
|
||||
/// reference or a reference via an augmented assignment.
|
||||
node_id: Option<NodeId>,
|
||||
/// The scope in which the reference is defined.
|
||||
scope_id: ScopeId,
|
||||
/// The range of the reference in the source code.
|
||||
|
@ -22,6 +25,11 @@ pub struct ResolvedReference {
|
|||
}
|
||||
|
||||
impl ResolvedReference {
|
||||
/// The expression that the reference occurs in.
|
||||
pub const fn expression_id(&self) -> Option<NodeId> {
|
||||
self.node_id
|
||||
}
|
||||
|
||||
/// The scope in which the reference is defined.
|
||||
pub const fn scope_id(&self) -> ScopeId {
|
||||
self.scope_id
|
||||
|
@ -35,6 +43,48 @@ impl ResolvedReference {
|
|||
ExecutionContext::Runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a typing-only type annotation.
|
||||
pub const fn in_typing_only_annotation(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::TYPING_ONLY_ANNOTATION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a runtime-required type annotation.
|
||||
pub const fn in_runtime_evaluated_annotation(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::RUNTIME_EVALUATED_ANNOTATION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a "simple" string type definition.
|
||||
pub const fn in_simple_string_type_definition(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::SIMPLE_STRING_TYPE_DEFINITION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a "complex" string type definition.
|
||||
pub const fn in_complex_string_type_definition(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::COMPLEX_STRING_TYPE_DEFINITION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a `__future__` type definition.
|
||||
pub const fn in_future_type_definition(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::FUTURE_TYPE_DEFINITION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in any kind of deferred type definition.
|
||||
pub const fn in_deferred_type_definition(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::DEFERRED_TYPE_DEFINITION)
|
||||
}
|
||||
|
||||
/// Return `true` if the context is in a type-checking block.
|
||||
pub const fn in_type_checking_block(&self) -> bool {
|
||||
self.flags
|
||||
.intersects(SemanticModelFlags::TYPE_CHECKING_BLOCK)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ResolvedReference {
|
||||
|
@ -57,10 +107,12 @@ impl ResolvedReferences {
|
|||
pub(crate) fn push(
|
||||
&mut self,
|
||||
scope_id: ScopeId,
|
||||
node_id: Option<NodeId>,
|
||||
range: TextRange,
|
||||
flags: SemanticModelFlags,
|
||||
) -> ResolvedReferenceId {
|
||||
self.0.push(ResolvedReference {
|
||||
node_id,
|
||||
scope_id,
|
||||
range,
|
||||
flags,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue