[ruff] Extend FA102 with listed PEP 585-compatible APIs (#20659)

Resolves https://github.com/astral-sh/ruff/issues/20512

This PR expands FA102’s preview coverage to flag every
PEP 585-compatible API that breaks without from `from __future__ import
annotations`, including `collections.abc`. The rule now treats asyncio
futures, pathlib-style queues, weakref containers, shelve proxies, and
the full `collections.abc` family as generics once preview mode is
enabled.

Stable behavior is unchanged; the broader matching runs behind
`is_future_required_preview_generics_enabled`, letting us vet the new
diagnostics before marking them as stable.

I've also added a snapshot test that covers all of the newly supported
types.

Check out
https://docs.python.org/3/library/stdtypes.html#standard-generic-classes
for a list of commonly used PEP 585-compatible APIs.
This commit is contained in:
liam 2025-10-03 09:45:32 -04:00 committed by GitHub
parent 7d7237c660
commit ebfb33c30b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1049 additions and 27 deletions

View file

@ -8,11 +8,10 @@ use ruff_python_ast::{
StmtAssign,
};
use ruff_python_stdlib::typing::{
as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type,
is_immutable_non_generic_type, is_immutable_return_type, is_literal_member,
is_mutable_return_type, is_pep_593_generic_member, is_pep_593_generic_type,
is_standard_library_generic, is_standard_library_generic_member, is_standard_library_literal,
is_typed_dict, is_typed_dict_member,
as_pep_585_generic, is_immutable_generic_type, is_immutable_non_generic_type,
is_immutable_return_type, is_literal_member, is_mutable_return_type, is_pep_593_generic_member,
is_pep_593_generic_type, is_standard_library_generic, is_standard_library_generic_member,
is_standard_library_literal, is_typed_dict, is_typed_dict_member,
};
use ruff_text_size::Ranged;
use smallvec::{SmallVec, smallvec};
@ -148,14 +147,46 @@ pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<Module
}
/// Return whether a given expression uses a PEP 585 standard library generic.
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
pub fn is_pep585_generic(
expr: &Expr,
semantic: &SemanticModel,
include_preview_generics: bool,
) -> bool {
semantic
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
let [module, name] = qualified_name.segments() else {
return false;
};
has_pep_585_generic(module, name)
.is_some_and(|qualified_name| match qualified_name.segments() {
["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"]
| ["collections", "deque" | "defaultdict"] => true,
["asyncio", "Future" | "Task"]
| ["collections", "ChainMap" | "Counter" | "OrderedDict"]
| [
"contextlib",
"AbstractAsyncContextManager" | "AbstractContextManager",
]
| ["dataclasses", "Field"]
| ["functools", "cached_property" | "partialmethod"]
| ["os", "PathLike"]
| [
"queue",
"LifoQueue" | "PriorityQueue" | "Queue" | "SimpleQueue",
]
| ["re", "Match" | "Pattern"]
| ["shelve", "BsdDbShelf" | "DbfilenameShelf" | "Shelf"]
| ["types", "MappingProxyType"]
| [
"weakref",
"WeakKeyDictionary" | "WeakMethod" | "WeakSet" | "WeakValueDictionary",
]
| [
"collections",
"abc",
"AsyncGenerator" | "AsyncIterable" | "AsyncIterator" | "Awaitable" | "ByteString"
| "Callable" | "Collection" | "Container" | "Coroutine" | "Generator" | "ItemsView"
| "Iterable" | "Iterator" | "KeysView" | "Mapping" | "MappingView"
| "MutableMapping" | "MutableSequence" | "MutableSet" | "Reversible" | "Sequence"
| "Set" | "ValuesView",
] => include_preview_generics,
_ => false,
})
}