diff --git a/resources/test/fixtures/F821_1.py b/resources/test/fixtures/F821_1.py index 3a562037e3..a503ad4679 100644 --- a/resources/test/fixtures/F821_1.py +++ b/resources/test/fixtures/F821_1.py @@ -16,3 +16,15 @@ import typing # F821 Undefined name `Model` x = typing.cast("Model", x) + + +from typing import Pattern + +# F821 Undefined name `Model` +x = Pattern["Model"] + + +from typing.re import Match + +# F821 Undefined name `Model` +x = Match["Model"] diff --git a/src/check_ast.rs b/src/check_ast.rs index acf7c19152..6e8f866b1d 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -37,7 +37,14 @@ use crate::{ }; const GLOBAL_SCOPE_INDEX: usize = 0; -const TRACK_FROM_IMPORTS: [&str; 1] = ["typing"]; +const TRACK_FROM_IMPORTS: [&str; 6] = [ + "collections", + "collections.abc", + "contextlib", + "re", + "typing", + "typing.re", +]; pub struct Checker<'a> { // Input data. @@ -1396,7 +1403,7 @@ where } } ExprKind::Subscript { value, slice, ctx } => { - match typing::match_annotated_subscript(value, self.from_imports.get("typing")) { + match typing::match_annotated_subscript(value, &self.from_imports) { Some(subscript) => match subscript { // Ex) Optional[int] SubscriptKind::AnnotatedSubscript => { diff --git a/src/python/typing.rs b/src/python/typing.rs index a0409a3054..f1d8c5bda3 100644 --- a/src/python/typing.rs +++ b/src/python/typing.rs @@ -1,100 +1,132 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use once_cell::sync::Lazy; use rustpython_ast::{Expr, ExprKind}; -// TODO(charlie): Some of these are actually from `collections`. -// Review: https://peps.python.org/pep-0585/. -static ANNOTATED_SUBSCRIPTS: Lazy> = Lazy::new(|| { - BTreeSet::from([ - "AbstractAsyncContextManager", - "AbstractContextManager", - "AbstractSet", - // "Annotated", - "AsyncContextManager", - "AsyncGenerator", - "AsyncIterable", - "AsyncIterator", - "Awaitable", - "BinaryIO", - "ByteString", - "Callable", - "ChainMap", - "ClassVar", - "Collection", - "Concatenate", - "Container", - "ContextManager", - "Coroutine", - "Counter", - "Counter", - "DbfilenameShelf", - "DefaultDict", - "Deque", - "Dict", - "Field", - "Final", - "FrozenSet", - "Generator", - "Iterator", - "Generic", - "IO", - "ItemsView", - "Iterable", - "Iterator", - "KeysView", - "LifoQueue", - "List", - "Mapping", - "MappingProxyType", - "MappingView", - "Match", - "MutableMapping", - "MutableSequence", - "MutableSet", - "Optional", - "OrderedDict", - "PathLike", - "Pattern", - "PriorityQueue", - "Protocol", - "Queue", - "Reversible", - "Sequence", - "Set", - "Shelf", - "SimpleQueue", - "TextIO", - "Tuple", - "Type", - "TypeGuard", - "Union", - "ValuesView", - "WeakKeyDictionary", - "WeakMethod", - "WeakSet", - "WeakValueDictionary", - "defaultdict", - "deque", - "dict", - "frozenset", - "list", - "set", - "tuple", - "type", - ]) -}); +// See: https://docs.python.org/3/library/typing.html +static IMPORTED_SUBSCRIPTS: Lazy>> = + Lazy::new(|| { + let mut import_map = BTreeMap::new(); + for (name, module) in [ + // `collections` + ("ChainMap", "collections"), + ("Counter", "collections"), + ("OrderedDict", "collections"), + ("defaultdict", "collections"), + ("deque", "collections"), + // `collections.abc` + ("AsyncGenerator", "collections.abc"), + ("AsyncIterable", "collections.abc"), + ("AsyncIterator", "collections.abc"), + ("Awaitable", "collections.abc"), + ("ByteString", "collections.abc"), + ("Callable", "collections.abc"), + ("Collection", "collections.abc"), + ("Container", "collections.abc"), + ("Coroutine", "collections.abc"), + ("Generator", "collections.abc"), + ("ItemsView", "collections.abc"), + ("Iterable", "collections.abc"), + ("Iterator", "collections.abc"), + ("KeysView", "collections.abc"), + ("Mapping", "collections.abc"), + ("MappingView", "collections.abc"), + ("MutableMapping", "collections.abc"), + ("MutableSequence", "collections.abc"), + ("MutableSet", "collections.abc"), + ("Reversible", "collections.abc"), + ("Sequence", "collections.abc"), + ("Set", "collections.abc"), + ("ValuesView", "collections.abc"), + // `contextlib` + ("AbstractAsyncContextManager", "contextlib"), + ("AbstractContextManager", "contextlib"), + // `re` + ("Match", "re"), + ("Pattern", "re"), + // `typing` + ("AbstractSet", "typing"), + ("Annotated", "typing"), + ("AsyncContextManager", "typing"), + ("AsyncContextManager", "typing"), + ("AsyncGenerator", "typing"), + ("AsyncIterable", "typing"), + ("AsyncIterator", "typing"), + ("Awaitable", "typing"), + ("BinaryIO", "typing"), + ("ByteString", "typing"), + ("Callable", "typing"), + ("ChainMap", "typing"), + ("ClassVar", "typing"), + ("Collection", "typing"), + ("Concatenate", "typing"), + ("Container", "typing"), + ("ContextManager", "typing"), + ("ContextManager", "typing"), + ("Coroutine", "typing"), + ("Counter", "typing"), + ("DefaultDict", "typing"), + ("Deque", "typing"), + ("Dict", "typing"), + ("Final", "typing"), + ("FrozenSet", "typing"), + ("Generator", "typing"), + ("Generic", "typing"), + ("IO", "typing"), + ("ItemsView", "typing"), + ("Iterable", "typing"), + ("Iterator", "typing"), + ("KeysView", "typing"), + ("List", "typing"), + ("Mapping", "typing"), + ("Match", "typing"), + ("MutableMapping", "typing"), + ("MutableSequence", "typing"), + ("MutableSet", "typing"), + ("Optional", "typing"), + ("OrderedDict", "typing"), + ("Pattern", "typing"), + ("Reversible", "typing"), + ("Sequence", "typing"), + ("Set", "typing"), + ("TextIO", "typing"), + ("Tuple", "typing"), + ("Type", "typing"), + ("Type", "typing"), + ("TypeGuard", "typing"), + ("Union", "typing"), + ("Unpack", "typing"), + ("ValuesView", "typing"), + // `typing.io` + ("BinaryIO", "typing.io"), + ("IO", "typing.io"), + ("TextIO", "typing.io"), + // `typing.re` + ("Match", "typing.re"), + ("Pattern", "typing.re"), + // `weakref` + ("WeakKeyDictionary", "weakref"), + ("WeakSet", "weakref"), + ("WeakValueDictionary", "weakref"), + ] { + import_map + .entry(name) + .or_insert_with(BTreeSet::new) + .insert(module); + } + import_map + }); +// These are all assumed to come from the `typing` module. +// See: https://peps.python.org/pep-0585/ static PEP_585_BUILTINS_ELIGIBLE: Lazy> = Lazy::new(|| BTreeSet::from(["Dict", "FrozenSet", "List", "Set", "Tuple", "Type"])); +// These are all assumed to come from the `typing` module. +// See: https://peps.python.org/pep-0585/ static PEP_585_BUILTINS: Lazy> = Lazy::new(|| BTreeSet::from(["dict", "frozenset", "list", "set", "tuple", "type"])); -fn is_annotated_subscript(name: &str) -> bool { - ANNOTATED_SUBSCRIPTS.contains(name) -} - fn is_pep593_annotated_subscript(name: &str) -> bool { name == "Annotated" } @@ -106,17 +138,23 @@ pub enum SubscriptKind { pub fn match_annotated_subscript( expr: &Expr, - imports: Option<&BTreeSet<&str>>, + imports: &BTreeMap<&str, BTreeSet<&str>>, ) -> Option { match &expr.node { ExprKind::Attribute { attr, value, .. } => { if let ExprKind::Name { id, .. } = &value.node { - if id == "typing" { - if is_pep593_annotated_subscript(attr) { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } else if is_annotated_subscript(attr) { - return Some(SubscriptKind::AnnotatedSubscript); - } + // If `id` is `typing` and `attr` is `Union`, verify that `typing.Union` is an + // annotated subscript. + if IMPORTED_SUBSCRIPTS + .get(&attr.as_str()) + .map(|imports| imports.contains(&id.as_str())) + .unwrap_or_default() + { + return if is_pep593_annotated_subscript(attr) { + Some(SubscriptKind::PEP593AnnotatedSubscript) + } else { + Some(SubscriptKind::AnnotatedSubscript) + }; } } } @@ -126,14 +164,20 @@ pub fn match_annotated_subscript( return Some(SubscriptKind::AnnotatedSubscript); } - if imports - .map(|import| import.contains(&id.as_str())) - .unwrap_or_default() - { - if is_pep593_annotated_subscript(id) { - return Some(SubscriptKind::PEP593AnnotatedSubscript); - } else if is_annotated_subscript(id) { - return Some(SubscriptKind::AnnotatedSubscript); + // Verify that, e.g., `Union` is a reference to `typing.Union`. + if let Some(modules) = IMPORTED_SUBSCRIPTS.get(&id.as_str()) { + for module in modules { + if imports + .get(module) + .map(|imports| imports.contains(&id.as_str())) + .unwrap_or_default() + { + return if is_pep593_annotated_subscript(id) { + Some(SubscriptKind::PEP593AnnotatedSubscript) + } else { + Some(SubscriptKind::AnnotatedSubscript) + }; + } } } } @@ -142,8 +186,8 @@ pub fn match_annotated_subscript( None } -/// Returns `true` if `Expr` represents a reference to a typing object with a PEP585 built-in. -pub fn is_pep585_builtin(expr: &Expr, imports: Option<&BTreeSet<&str>>) -> bool { +/// Returns `true` if `Expr` represents a reference to a typing object with a PEP 585 built-in. +pub fn is_pep585_builtin(expr: &Expr, typing_imports: Option<&BTreeSet<&str>>) -> bool { match &expr.node { ExprKind::Attribute { attr, value, .. } => { if let ExprKind::Name { id, .. } = &value.node { @@ -153,8 +197,8 @@ pub fn is_pep585_builtin(expr: &Expr, imports: Option<&BTreeSet<&str>>) -> bool } } ExprKind::Name { id, .. } => { - imports - .map(|import| import.contains(&id.as_str())) + typing_imports + .map(|imports| imports.contains(&id.as_str())) .unwrap_or_default() && PEP_585_BUILTINS_ELIGIBLE.contains(&id.as_str()) } diff --git a/src/snapshots/ruff__linter__tests__F821_F821_1.py.snap b/src/snapshots/ruff__linter__tests__F821_F821_1.py.snap index 3c25cf29b6..53a4ffc5d1 100644 --- a/src/snapshots/ruff__linter__tests__F821_F821_1.py.snap +++ b/src/snapshots/ruff__linter__tests__F821_F821_1.py.snap @@ -20,4 +20,22 @@ expression: checks row: 18 column: 23 fix: ~ +- kind: + UndefinedName: Model + location: + row: 24 + column: 12 + end_location: + row: 24 + column: 19 + fix: ~ +- kind: + UndefinedName: Model + location: + row: 30 + column: 10 + end_location: + row: 30 + column: 17 + fix: ~