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:
Charlie Marsh 2023-12-12 22:12:38 -05:00 committed by GitHub
parent 4d2ee5bf98
commit 1a65e544c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1034 additions and 208 deletions

View file

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

View file

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