diff --git a/Cargo.lock b/Cargo.lock index 693c3d5e34..212a9121b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,6 +1759,7 @@ dependencies = [ "filetime", "glob", "itertools", + "lazy_static", "log", "notify", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 4bc6f07eac..0b5fd04c71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ fern = { version = "0.6.1" } filetime = { version = "0.2.17" } glob = { version = "0.3.0" } itertools = "0.10.3" +lazy_static = "1.4.0" log = { version = "0.4.17" } notify = { version = "4.0.17" } once_cell = { version = "1.13.1" } diff --git a/resources/test/fixtures/F401.py b/resources/test/fixtures/F401.py index 3c83f71679..2b4df1d0db 100644 --- a/resources/test/fixtures/F401.py +++ b/resources/test/fixtures/F401.py @@ -11,10 +11,13 @@ import multiprocessing.pool import multiprocessing.process import logging.config import logging.handlers -from typing import NamedTuple, Dict, Type, TypeVar, List, Set +from typing import TYPING_CHECK, NamedTuple, Dict, Type, TypeVar, List, Set, Union, cast from blah import ClassA, ClassB, ClassC +if TYPING_CHECK: + from models import Fruit, Nut, Vegetable + class X: datetime: datetime @@ -32,3 +35,7 @@ __all__ += ["ClassC"] X = TypeVar("X") Y = TypeVar("Y", bound="Dict") Z = TypeVar("Z", "List", "Set") + +a = list["Fruit"] +b = Union["Nut", None] +c = cast("Vegetable", b) diff --git a/src/check_ast.rs b/src/check_ast.rs index 1c377f1a5b..e2b8fa6567 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -12,8 +12,9 @@ use crate::ast::types::{Binding, BindingKind, Scope, ScopeKind}; use crate::ast::visitor::{walk_excepthandler, Visitor}; use crate::ast::{checks, visitor}; use crate::autofix::fixer; -use crate::builtins::{BUILTINS, MAGIC_GLOBALS}; use crate::checks::{Check, CheckCode, CheckKind}; +use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS}; +use crate::python::typing; use crate::settings::Settings; pub const GLOBAL_SCOPE_INDEX: usize = 0; @@ -82,6 +83,14 @@ fn match_name_or_attr(expr: &Expr, target: &str) -> bool { } } +fn is_annotated_subscript(expr: &Expr) -> bool { + match &expr.node { + ExprKind::Attribute { attr, .. } => typing::is_annotated_subscript(attr), + ExprKind::Name { id, .. } => typing::is_annotated_subscript(id), + _ => false, + } +} + impl<'a, 'b> Visitor<'b> for Checker<'a> where 'b: 'a, @@ -544,7 +553,25 @@ where args, keywords, } => { - if match_name_or_attr(func, "TypeVar") { + if match_name_or_attr(func, "ForwardRef") { + self.visit_expr(func); + for expr in args { + self.visit_annotation(expr); + } + } else if match_name_or_attr(func, "cast") { + self.visit_expr(func); + if !args.is_empty() { + self.visit_annotation(&args[0]); + } + for expr in args.iter().skip(1) { + self.visit_expr(expr); + } + } else if match_name_or_attr(func, "NewType") { + self.visit_expr(func); + for expr in args.iter().skip(1) { + self.visit_annotation(expr); + } + } else if match_name_or_attr(func, "TypeVar") { self.visit_expr(func); for expr in args.iter().skip(1) { self.visit_annotation(expr); @@ -559,7 +586,38 @@ where } } } + } else if match_name_or_attr(func, "NamedTuple") { + self.visit_expr(func); + + // NamedTuple("a", [("a", int)]) + if args.len() > 1 { + match &args[1].node { + ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => { + for elt in elts { + match &elt.node { + ExprKind::List { elts, .. } + | ExprKind::Tuple { elts, .. } => { + if elts.len() == 2 { + self.visit_expr(&elts[0]); + self.visit_annotation(&elts[1]); + } + } + _ => {} + } + } + } + _ => {} + } + } + + // NamedTuple("a", a=int) + for keyword in keywords { + let KeywordData { value, .. } = &keyword.node; + self.visit_annotation(value); + } } else if match_name_or_attr(func, "TypedDict") { + self.visit_expr(func); + // TypedDict("a", {"a": int}) if args.len() > 1 { if let ExprKind::Dict { keys, values } = &args[1].node { @@ -582,7 +640,7 @@ where } } ExprKind::Subscript { value, slice, ctx } => { - if match_name_or_attr(value, "Type") { + if is_annotated_subscript(value) { self.visit_expr(value); self.visit_annotation(slice); self.visit_expr_context(ctx); diff --git a/src/lib.rs b/src/lib.rs index bdd726bc63..600d7f9ec3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ extern crate core; mod ast; mod autofix; -mod builtins; mod cache; pub mod check_ast; mod check_lines; @@ -12,4 +11,5 @@ pub mod linter; pub mod logging; pub mod message; mod pyproject; +mod python; pub mod settings; diff --git a/src/python.rs b/src/python.rs new file mode 100644 index 0000000000..a84b39dbec --- /dev/null +++ b/src/python.rs @@ -0,0 +1,2 @@ +pub mod builtins; +pub mod typing; diff --git a/src/builtins.rs b/src/python/builtins.rs similarity index 100% rename from src/builtins.rs rename to src/python/builtins.rs index d398bab137..74f33ca157 100644 --- a/src/builtins.rs +++ b/src/python/builtins.rs @@ -156,8 +156,8 @@ pub const BUILTINS: &[&str] = &[ // Globally defined names which are not attributes of the builtins module, or are only present on // some platforms. pub const MAGIC_GLOBALS: &[&str] = &[ - "__file__", - "__builtins__", - "__annotations__", "WindowsError", + "__annotations__", + "__builtins__", + "__file__", ]; diff --git a/src/python/typing.rs b/src/python/typing.rs new file mode 100644 index 0000000000..dcf7c4f548 --- /dev/null +++ b/src/python/typing.rs @@ -0,0 +1,88 @@ +use lazy_static::lazy_static; +use std::collections::BTreeSet; + +lazy_static! { + static ref ANNOTATED_SUBSCRIPTS: BTreeSet<&'static str> = BTreeSet::from([ + "AbstractAsyncContextManager", + "AbstractContextManager", + "AbstractSet", + "AsyncContextManager", + "AsyncGenerator", + "AsyncIterable", + "AsyncIterator", + "Awaitable", + "BinaryIO", + "BsdDbShelf", + "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", + "cached_property", + "defaultdict", + "deque", + "dict", + "frozenset", + "list", + "partialmethod", + "set", + "tuple", + "type", + ]); +} + +pub fn is_annotated_subscript(name: &str) -> bool { + ANNOTATED_SUBSCRIPTS.contains(name) +}