From 4e3ef35fd9851fc358844fa45594d15630314020 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 2 Feb 2024 15:32:06 -0500 Subject: [PATCH] Short-circuit typing matches based on imports --- .../boolean_type_hint_positional_argument.rs | 5 ++ .../flake8_pyi/rules/prefix_type_params.rs | 74 ++++++++++--------- .../src/rules/flake8_type_checking/helpers.rs | 6 +- .../src/rules/pep8_naming/helpers.rs | 12 +++ .../rules/pylint/rules/redefined_loop_name.rs | 4 + .../src/rules/pylint/rules/type_bivariance.rs | 5 ++ .../rules/type_name_incorrect_variance.rs | 5 ++ .../pylint/rules/type_param_name_mismatch.rs | 5 ++ .../src/rules/ruff/rules/helpers.rs | 18 ++++- .../src/analyze/typing.rs | 5 ++ crates/ruff_python_semantic/src/model.rs | 37 ++++++---- 11 files changed, 128 insertions(+), 48 deletions(-) diff --git a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs index 1322deb207f4f..f4bf4a2963857 100644 --- a/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_boolean_trap/rules/boolean_type_hint_positional_argument.rs @@ -191,6 +191,11 @@ fn match_annotation_to_complex_bool(annotation: &Expr, semantic: &SemanticModel) } // Ex) `typing.Union[bool, int]` Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + // If the typing modules were never imported, we'll never match below. + if !semantic.seen_typing() { + return false; + } + let call_path = semantic.resolve_call_path(value); if call_path .as_ref() diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs index 299594124e2e6..670a30ce39c6a 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -1,9 +1,8 @@ use std::fmt; -use ruff_python_ast::{self as ast, Expr}; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast, Expr}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -61,44 +60,53 @@ impl Violation for UnprefixedTypeParam { /// PYI001 pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + // If the typing modules were never imported, we'll never match below. + if !checker.semantic().seen_typing() { + return; + } + let [target] = targets else { return; }; + if let Expr::Name(ast::ExprName { id, .. }) = target { if id.starts_with('_') { return; } }; - if let Expr::Call(ast::ExprCall { func, .. }) = value { - let Some(kind) = checker - .semantic() - .resolve_call_path(func) - .and_then(|call_path| { - if checker - .semantic() - .match_typing_call_path(&call_path, "ParamSpec") - { - Some(VarKind::ParamSpec) - } else if checker - .semantic() - .match_typing_call_path(&call_path, "TypeVar") - { - Some(VarKind::TypeVar) - } else if checker - .semantic() - .match_typing_call_path(&call_path, "TypeVarTuple") - { - Some(VarKind::TypeVarTuple) - } else { - None - } - }) - else { - return; - }; - checker - .diagnostics - .push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range())); - } + let Expr::Call(ast::ExprCall { func, .. }) = value else { + return; + }; + + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else { + None + } + }) + else { + return; + }; + + checker + .diagnostics + .push(Diagnostic::new(UnprefixedTypeParam { kind }, value.range())); } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs index cd0d67b11183b..21a4aecd83ef9 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs @@ -6,7 +6,7 @@ use ruff_python_ast::helpers::{map_callable, map_subscript}; use ruff_python_ast::{self as ast, Decorator, Expr}; use ruff_python_codegen::{Generator, Stylist}; use ruff_python_semantic::{ - analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel, + analyze, Binding, BindingKind, Modules, NodeId, ResolvedReference, ScopeKind, SemanticModel, }; use ruff_source_file::Locator; use ruff_text_size::Ranged; @@ -111,6 +111,10 @@ fn runtime_required_decorators( /// /// See: pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_module(Modules::DATACLASSES) { + return false; + } + // Determine whether the assignment is in a `@dataclass` class definition. if let ScopeKind::Class(class_def) = semantic.current_scope().kind { if class_def.decorator_list.iter().any(|decorator| { diff --git a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs index b48388935cfff..3aff439681648 100644 --- a/crates/ruff_linter/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff_linter/src/rules/pep8_naming/helpers.rs @@ -39,6 +39,10 @@ pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) - /// Returns `true` if the statement is an assignment to a `TypedDict`. pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; @@ -50,6 +54,10 @@ pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> /// Returns `true` if the statement is an assignment to a `TypeVar` or `NewType`. pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; @@ -75,6 +83,10 @@ pub(super) fn is_type_alias_assignment(stmt: &Stmt, semantic: &SemanticModel) -> /// Returns `true` if the statement is an assignment to a `TypedDict`. pub(super) fn is_typed_dict_class(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + arguments.is_some_and(|arguments| { arguments .args diff --git a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs index 92d6c7b6f336c..0c85d1a8ed0c4 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/redefined_loop_name.rs @@ -233,6 +233,10 @@ impl<'a, 'b> StatementVisitor<'b> for InnerForWithAssignTargetsVisitor<'a, 'b> { /// x = cast(int, x) /// ``` fn assignment_is_cast_expr(value: &Expr, target: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + let Expr::Call(ast::ExprCall { func, arguments: Arguments { args, .. }, diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs index ffe53bc5e7f5c..4f95f0f603e47 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs @@ -74,6 +74,11 @@ impl Violation for TypeBivariance { /// PLC0131 pub(crate) fn type_bivariance(checker: &mut Checker, value: &Expr) { + // If the typing modules were never imported, we'll never match below. + if !checker.semantic().seen_typing() { + return; + } + let Expr::Call(ast::ExprCall { func, arguments, .. }) = value diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs index ff5d9feaf2861..1995eff0f58d1 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_name_incorrect_variance.rs @@ -65,6 +65,11 @@ impl Violation for TypeNameIncorrectVariance { /// PLC0105 pub(crate) fn type_name_incorrect_variance(checker: &mut Checker, value: &Expr) { + // If the typing modules were never imported, we'll never match below. + if !checker.semantic().seen_typing() { + return; + } + let Expr::Call(ast::ExprCall { func, arguments, .. }) = value diff --git a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs index a3afd61505e34..cad3db6de4b55 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/type_param_name_mismatch.rs @@ -59,6 +59,11 @@ impl Violation for TypeParamNameMismatch { /// PLC0132 pub(crate) fn type_param_name_mismatch(checker: &mut Checker, value: &Expr, targets: &[Expr]) { + // If the typing modules were never imported, we'll never match below. + if !checker.semantic().seen_typing() { + return; + } + let [target] = targets else { return; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs index c646509435945..5aba7b8e0dfde 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/helpers.rs @@ -1,6 +1,6 @@ use ruff_python_ast::helpers::{map_callable, map_subscript}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::{analyze, BindingKind, SemanticModel}; +use ruff_python_semantic::{analyze, BindingKind, Modules, SemanticModel}; /// Return `true` if the given [`Expr`] is a special class attribute, like `__slots__`. /// @@ -20,6 +20,10 @@ pub(super) fn is_special_attribute(value: &Expr) -> bool { /// Returns `true` if the given [`Expr`] is a `dataclasses.field` call. pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_module(Modules::DATACLASSES) { + return false; + } + semantic .resolve_call_path(func) .is_some_and(|call_path| matches!(call_path.as_slice(), ["dataclasses", "field"])) @@ -27,6 +31,10 @@ pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool /// Returns `true` if the given [`Expr`] is a `typing.ClassVar` annotation. pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + // ClassVar can be used either with a subscript `ClassVar[...]` or without (the type is // inferred). semantic.match_typing_expr(map_subscript(annotation), "ClassVar") @@ -34,6 +42,10 @@ pub(super) fn is_class_var_annotation(annotation: &Expr, semantic: &SemanticMode /// Returns `true` if the given [`Expr`] is a `typing.Final` annotation. pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool { + if !semantic.seen_typing() { + return false; + } + // Final can be used either with a subscript `Final[...]` or without (the type is // inferred). semantic.match_typing_expr(map_subscript(annotation), "Final") @@ -41,6 +53,10 @@ pub(super) fn is_final_annotation(annotation: &Expr, semantic: &SemanticModel) - /// Returns `true` if the given class is a dataclass. pub(super) fn is_dataclass(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool { + if !semantic.seen_module(Modules::DATACLASSES) { + return false; + } + class_def.decorator_list.iter().any(|decorator| { semantic .resolve_call_path(map_callable(&decorator.expression)) diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 53e21969f59f7..3283db129d9bf 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -161,6 +161,11 @@ pub fn to_pep604_operator( } } + // If the typing modules were never imported, we'll never match below. + if !semantic.seen_typing() { + return None; + } + // If the slice is a forward reference (e.g., `Optional["Foo"]`), it can only be rewritten // if we're in a typing-only context. // diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 6446c3126d626..df5e3ab7d0d57 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -172,8 +172,10 @@ impl<'a> SemanticModel<'a> { /// Return `true` if the `Expr` is a reference to `typing.${target}`. pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool { - self.resolve_call_path(expr) - .is_some_and(|call_path| self.match_typing_call_path(&call_path, target)) + self.seen_typing() + && self + .resolve_call_path(expr) + .is_some_and(|call_path| self.match_typing_call_path(&call_path, target)) } /// Return `true` if the call path is a reference to `typing.${target}`. @@ -1088,22 +1090,24 @@ impl<'a> SemanticModel<'a> { /// the need to resolve symbols from these modules if they haven't been seen. pub fn add_module(&mut self, module: &str) { match module { - "trio" => self.seen.insert(Modules::TRIO), + "_typeshed" => self.seen.insert(Modules::TYPESHED), + "collections" => self.seen.insert(Modules::COLLECTIONS), + "dataclasses" => self.seen.insert(Modules::DATACLASSES), + "datetime" => self.seen.insert(Modules::DATETIME), + "django" => self.seen.insert(Modules::DJANGO), + "logging" => self.seen.insert(Modules::LOGGING), + "mock" => self.seen.insert(Modules::MOCK), "numpy" => self.seen.insert(Modules::NUMPY), + "os" => self.seen.insert(Modules::OS), "pandas" => self.seen.insert(Modules::PANDAS), "pytest" => self.seen.insert(Modules::PYTEST), - "django" => self.seen.insert(Modules::DJANGO), + "re" => self.seen.insert(Modules::RE), "six" => self.seen.insert(Modules::SIX), - "logging" => self.seen.insert(Modules::LOGGING), + "subprocess" => self.seen.insert(Modules::SUBPROCESS), + "tarfile" => self.seen.insert(Modules::TARFILE), + "trio" => self.seen.insert(Modules::TRIO), "typing" => self.seen.insert(Modules::TYPING), "typing_extensions" => self.seen.insert(Modules::TYPING_EXTENSIONS), - "tarfile" => self.seen.insert(Modules::TARFILE), - "re" => self.seen.insert(Modules::RE), - "collections" => self.seen.insert(Modules::COLLECTIONS), - "mock" => self.seen.insert(Modules::MOCK), - "os" => self.seen.insert(Modules::OS), - "datetime" => self.seen.insert(Modules::DATETIME), - "subprocess" => self.seen.insert(Modules::SUBPROCESS), _ => {} } } @@ -1118,6 +1122,11 @@ impl<'a> SemanticModel<'a> { self.seen.intersects(module) } + pub fn seen_typing(&self) -> bool { + self.seen_module(Modules::TYPING | Modules::TYPESHED | Modules::TYPING_EXTENSIONS) + || !self.typing_modules.is_empty() + } + /// Set the [`Globals`] for the current [`Scope`]. pub fn set_globals(&mut self, globals: Globals<'a>) { // If any global bindings don't already exist in the global scope, add them. @@ -1563,7 +1572,7 @@ impl ShadowedBinding { bitflags! { /// A select list of Python modules that the semantic model can explicitly track. #[derive(Debug)] - pub struct Modules: u16 { + pub struct Modules: u32 { const COLLECTIONS = 1 << 0; const DATETIME = 1 << 1; const DJANGO = 1 << 2; @@ -1580,6 +1589,8 @@ bitflags! { const TRIO = 1 << 13; const TYPING = 1 << 14; const TYPING_EXTENSIONS = 1 << 15; + const TYPESHED = 1 << 16; + const DATACLASSES = 1 << 17; } }