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

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