Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Short-circuit typing matches based on imports #9800

Merged
merged 1 commit into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()));
}
6 changes: 5 additions & 1 deletion crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +111,10 @@ fn runtime_required_decorators(
///
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
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| {
Expand Down
12 changes: 12 additions & 0 deletions crates/ruff_linter/src/rules/pep8_naming/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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;
};
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, .. },
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_linter/src/rules/pylint/rules/type_bivariance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
18 changes: 17 additions & 1 deletion crates/ruff_linter/src/rules/ruff/rules/helpers.rs
Original file line number Diff line number Diff line change
@@ -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__`.
///
Expand All @@ -20,27 +20,43 @@ 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"]))
}

/// 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")
}

/// 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")
}

/// 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))
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_python_semantic/src/analyze/typing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
37 changes: 24 additions & 13 deletions crates/ruff_python_semantic/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`.
Expand Down Expand Up @@ -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),
_ => {}
}
}
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}

Expand Down