From 76203ea4a88517fe840492f9ad8f0f744e4de003 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 1 Feb 2024 23:24:01 -0500 Subject: [PATCH] Track top-level module imports in the semantic model --- crates/ruff_linter/src/checkers/ast/mod.rs | 37 ++++++++----- .../flake8_2020/rules/name_or_attribute.rs | 8 ++- .../rules/tarfile_unsafe_members.rs | 9 ++-- .../rules/all_with_model_form.rs | 5 ++ .../rules/exclude_with_model_form.rs | 5 ++ .../rules/locals_in_render_function.rs | 6 ++- .../rules/model_without_dunder_str.rs | 6 ++- .../rules/non_leading_receiver_decorator.rs | 5 ++ .../rules/nullable_model_string_field.rs | 6 ++- .../rules/unordered_body_content_in_model.rs | 6 ++- .../rules/direct_logger_instantiation.rs | 5 ++ .../rules/invalid_get_logger_argument.rs | 5 ++ .../flake8_logging/rules/undocumented_warn.rs | 5 ++ .../rules/collections_named_tuple.rs | 5 ++ .../rules/async_function_with_timeout.rs | 11 ++-- .../src/rules/flake8_trio/rules/sync_call.rs | 5 ++ .../rules/timeout_without_await.rs | 5 ++ .../rules/flake8_trio/rules/unneeded_sleep.rs | 5 ++ .../flake8_trio/rules/zero_sleep_call.rs | 5 ++ .../rules/numpy/rules/deprecated_function.rs | 5 ++ .../numpy/rules/deprecated_type_alias.rs | 5 ++ .../src/rules/numpy/rules/legacy_random.rs | 5 ++ .../numpy/rules/numpy_2_0_deprecation.rs | 5 ++ .../src/rules/pandas_vet/rules/read_table.rs | 5 ++ .../pyupgrade/rules/typing_text_str_alias.rs | 5 ++ .../rules/refurb/rules/regex_flag_alias.rs | 5 ++ crates/ruff_python_semantic/src/model.rs | 54 +++++++++++++++---- 27 files changed, 197 insertions(+), 36 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index d13a7a9270c0b2..96a00417a503f2 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -349,13 +349,6 @@ where } } - // Track each top-level import, to guide import insertions. - if matches!(stmt, Stmt::Import(_) | Stmt::ImportFrom(_)) { - if self.semantic.at_top_level() { - self.importer.visit_import(stmt); - } - } - // Store the flags prior to any further descent, so that we can restore them after visiting // the node. let flags_snapshot = self.semantic.flags; @@ -371,14 +364,22 @@ where self.handle_node_load(target); } Stmt::Import(ast::StmtImport { names, range: _ }) => { + if self.semantic.at_top_level() { + self.importer.visit_import(stmt); + } + for alias in names { - if alias.name.contains('.') && alias.asname.is_none() { - // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be - // "foo.bar". - let name = alias.name.split('.').next().unwrap(); + // Given `import foo.bar`, `module` would be "foo", and `call_path` would be + // `["foo", "bar"]`. + let module = alias.name.split('.').next().unwrap(); + + // Mark the top-level module as "seen" by the semantic model. + self.semantic.see(module); + + if alias.asname.is_none() && alias.name.contains('.') { let call_path: Box<[&str]> = alias.name.split('.').collect(); self.add_binding( - name, + module, alias.identifier(), BindingKind::SubmoduleImport(SubmoduleImport { call_path }), BindingFlags::EXTERNAL, @@ -413,8 +414,20 @@ where level, range: _, }) => { + if self.semantic.at_top_level() { + self.importer.visit_import(stmt); + } + let module = module.as_deref(); let level = *level; + + // Mark the top-level module as "seen" by the semantic model. + if level.map_or(true, |level| level == 0) { + if let Some(module) = module.and_then(|module| module.split('.').next()) { + self.semantic.see(module); + } + } + for alias in names { if let Some("__future__") = module { let name = alias.asname.as_ref().unwrap_or(&alias.name); diff --git a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs index bdd1a7d4526602..5913151bcf2683 100644 --- a/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs +++ b/crates/ruff_linter/src/rules/flake8_2020/rules/name_or_attribute.rs @@ -1,7 +1,7 @@ -use ruff_python_ast::Expr; - use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -47,6 +47,10 @@ impl Violation for SixPY3 { /// YTT202 pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::SIX) { + return; + } + if checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs index b70083c8533c4b..978ae26051ee2e 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/tarfile_unsafe_members.rs @@ -3,6 +3,7 @@ use ruff_diagnostics::Diagnostic; use ruff_diagnostics::Violation; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; /// ## What it does @@ -48,6 +49,10 @@ impl Violation for TarfileUnsafeMembers { /// S202 pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall) { + if !checker.semantic().seen(Modules::TARFILE) { + return; + } + if !call .func .as_attribute_expr() @@ -65,10 +70,6 @@ pub(crate) fn tarfile_unsafe_members(checker: &mut Checker, call: &ast::ExprCall return; } - if !checker.semantic().seen(&["tarfile"]) { - return; - } - checker .diagnostics .push(Diagnostic::new(TarfileUnsafeMembers, call.func.range())); diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs index 8083575e2a59c6..9fd1d11919aa09 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/all_with_model_form.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -48,6 +49,10 @@ impl Violation for DjangoAllWithModelForm { /// DJ007 pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + if !is_model_form(class_def, checker.semantic()) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs index d1211c566210a6..737a6bf36a4dc3 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/exclude_with_model_form.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -46,6 +47,10 @@ impl Violation for DjangoExcludeWithModelForm { /// DJ006 pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + if !is_model_form(class_def, checker.semantic()) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs index c46657684d236a..b4fdb389dc7dd0 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -1,7 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr}; -use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -45,6 +45,10 @@ impl Violation for DjangoLocalsInRenderFunction { /// DJ003 pub(crate) fn locals_in_render_function(checker: &mut Checker, call: &ast::ExprCall) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + if !checker .semantic() .resolve_call_path(&call.func) diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs index 68e717f3c99010..2dd0889474a007 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -3,7 +3,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_true; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::{analyze, SemanticModel}; +use ruff_python_semantic::{analyze, Modules, SemanticModel}; use crate::checkers::ast::Checker; @@ -52,6 +52,10 @@ impl Violation for DjangoModelWithoutDunderStr { /// DJ008 pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + if !is_non_abstract_model(class_def, checker.semantic()) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index 8c7b3ab7a39d0c..7a7db458de7150 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Decorator; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -51,6 +52,10 @@ impl Violation for DjangoNonLeadingReceiverDecorator { /// DJ013 pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + let mut seen_receiver = false; for (i, decorator) in decorator_list.iter().enumerate() { let is_receiver = decorator.expression.as_call_expr().is_some_and(|call| { diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs index c1fcaac1445930..10c0185b3a1bd8 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::is_const_true; -use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -55,6 +55,10 @@ impl Violation for DjangoNullableModelStringField { /// DJ001 pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + for statement in body { let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { continue; diff --git a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index 635527dcaf0147..d03cd2e98f8aea 100644 --- a/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff_linter/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -3,7 +3,7 @@ use std::fmt; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr, Stmt}; -use ruff_python_semantic::SemanticModel; +use ruff_python_semantic::{Modules, SemanticModel}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -83,6 +83,10 @@ pub(crate) fn unordered_body_content_in_model( checker: &mut Checker, class_def: &ast::StmtClassDef, ) { + if !checker.semantic().seen(Modules::DJANGO) { + return; + } + if !helpers::is_model(class_def, checker.semantic()) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs index 552bee9c4fa5a0..a0c35ea42b8d0c 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/direct_logger_instantiation.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -54,6 +55,10 @@ impl Violation for DirectLoggerInstantiation { /// LOG001 pub(crate) fn direct_logger_instantiation(checker: &mut Checker, call: &ast::ExprCall) { + if !checker.semantic().seen(Modules::LOGGING) { + return; + } + if checker .semantic() .resolve_call_path(call.func.as_ref()) diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs index e7d4c8fd2ad5b0..ab7c69900e8393 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/invalid_get_logger_argument.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -57,6 +58,10 @@ impl Violation for InvalidGetLoggerArgument { /// LOG002 pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::ExprCall) { + if !checker.semantic().seen(Modules::LOGGING) { + return; + } + let Some(Expr::Name(expr @ ast::ExprName { id, .. })) = call.arguments.find_argument("name", 0) else { return; diff --git a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs index 134c4feef7d312..51f1ce691d4fcc 100644 --- a/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs +++ b/crates/ruff_linter/src/rules/flake8_logging/rules/undocumented_warn.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -48,6 +49,10 @@ impl Violation for UndocumentedWarn { /// LOG009 pub(crate) fn undocumented_warn(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::LOGGING) { + return; + } + if checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs index b045a443746e68..8a666b656f1db2 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/collections_named_tuple.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -50,6 +51,10 @@ impl Violation for CollectionsNamedTuple { /// PYI024 pub(crate) fn collections_named_tuple(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::COLLECTIONS) { + return; + } + if checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/async_function_with_timeout.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/async_function_with_timeout.rs index 048be94b03d5ac..5c1b39f0766729 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/async_function_with_timeout.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/async_function_with_timeout.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -48,15 +49,17 @@ pub(crate) fn async_function_with_timeout( if !function_def.is_async { return; } - let Some(timeout) = function_def.parameters.find("timeout") else { - return; - }; // If `trio` isn't in scope, avoid raising the diagnostic. - if !checker.semantic().seen(&["trio"]) { + if !checker.semantic().seen(Modules::TRIO) { return; } + // If the function doesn't have a `timeout` parameter, avoid raising the diagnostic. + let Some(timeout) = function_def.parameters.find("timeout") else { + return; + }; + checker.diagnostics.push(Diagnostic::new( TrioAsyncFunctionWithTimeout, timeout.range(), diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs index 5d7dc64bcbc3da..b248b67d497827 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{Expr, ExprCall}; +use ruff_python_semantic::Modules; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -51,6 +52,10 @@ impl Violation for TrioSyncCall { /// TRIO105 pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) { + if !checker.semantic().seen(Modules::TRIO) { + return; + } + let Some(method_name) = ({ let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else { return; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs index 6870d99f1abf1c..bcad6ffe77a670 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs @@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::AwaitVisitor; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{StmtWith, WithItem}; +use ruff_python_semantic::Modules; use crate::checkers::ast::Checker; use crate::rules::flake8_trio::method_name::MethodName; @@ -49,6 +50,10 @@ pub(crate) fn timeout_without_await( with_stmt: &StmtWith, with_items: &[WithItem], ) { + if !checker.semantic().seen(Modules::TRIO) { + return; + } + let Some(method_name) = with_items.iter().find_map(|item| { let call = item.context_expr.as_call_expr()?; let call_path = checker.semantic().resolve_call_path(call.func.as_ref())?; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs index d7387f5835d960..fe0845f14df021 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/unneeded_sleep.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -42,6 +43,10 @@ impl Violation for TrioUnneededSleep { /// TRIO110 pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) { + if !checker.semantic().seen(Modules::TRIO) { + return; + } + // The body should be a single `await` call. let [stmt] = while_stmt.body.as_slice() else { return; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs index a543ad5a9ae269..ae818b33004e49 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs @@ -2,6 +2,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number}; use ruff_python_semantic::analyze::typing::find_assigned_value; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -47,6 +48,10 @@ impl AlwaysFixableViolation for TrioZeroSleepCall { /// TRIO115 pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) { + if !checker.semantic().seen(Modules::TRIO) { + return; + } + if call.arguments.len() != 1 { return; } diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs index 7a106716541f93..ee226b4a438b80 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_function.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -55,6 +56,10 @@ impl Violation for NumpyDeprecatedFunction { /// NPY003 pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::NUMPY) { + return; + } + if let Some((existing, replacement)) = checker .semantic() diff --git a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs index 265a04d2ffb2fb..cf99b1ec2319e9 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/deprecated_type_alias.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -49,6 +50,10 @@ impl Violation for NumpyDeprecatedTypeAlias { /// NPY001 pub(crate) fn deprecated_type_alias(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::NUMPY) { + return; + } + if let Some(type_name) = checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs index d76507a8c709e8..4d40276c6b5241 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/legacy_random.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -59,6 +60,10 @@ impl Violation for NumpyLegacyRandom { /// NPY002 pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::NUMPY) { + return; + } + if let Some(method_name) = checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs index d13773e8516072..342957eee6edd9 100644 --- a/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs +++ b/crates/ruff_linter/src/rules/numpy/rules/numpy_2_0_deprecation.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -152,6 +153,10 @@ enum Compatibility { } /// NPY201 pub(crate) fn numpy_2_0_deprecation(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::NUMPY) { + return; + } + let maybe_replacement = checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs index f7b5fa9300e6b1..b07c7c7c2077d4 100644 --- a/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs +++ b/crates/ruff_linter/src/rules/pandas_vet/rules/read_table.rs @@ -2,6 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast as ast; use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -46,6 +47,10 @@ impl Violation for PandasUseOfDotReadTable { /// PD012 pub(crate) fn use_of_read_table(checker: &mut Checker, call: &ast::ExprCall) { + if !checker.semantic().seen(Modules::PANDAS) { + return; + } + if checker .semantic() .resolve_call_path(&call.func) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs index 86d9e6b28bf990..912907d96604e7 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/typing_text_str_alias.rs @@ -2,6 +2,7 @@ use ruff_python_ast::Expr; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -46,6 +47,10 @@ impl Violation for TypingTextStrAlias { /// UP019 pub(crate) fn typing_text_str_alias(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::TYPING) { + return; + } + if checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs index bd3fcdd56c64a3..5bcc338f8c01b9 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/regex_flag_alias.rs @@ -1,6 +1,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::Expr; +use ruff_python_semantic::Modules; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; @@ -52,6 +53,10 @@ impl AlwaysFixableViolation for RegexFlagAlias { /// FURB167 pub(crate) fn regex_flag_alias(checker: &mut Checker, expr: &Expr) { + if !checker.semantic().seen(Modules::RE) { + return; + } + let Some(flag) = checker .semantic() diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 3e15e62aff7d26..a82179143582ec 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -120,6 +120,9 @@ pub struct SemanticModel<'a> { /// Flags for the semantic model. pub flags: SemanticModelFlags, + /// Modules that have been seen by the semantic model. + pub seen: Modules, + /// Exceptions that have been handled by the current scope. pub handled_exceptions: Vec, @@ -149,11 +152,36 @@ impl<'a> SemanticModel<'a> { delayed_annotations: FxHashMap::default(), rebinding_scopes: FxHashMap::default(), flags: SemanticModelFlags::new(path), + seen: Modules::empty(), handled_exceptions: Vec::default(), resolved_names: FxHashMap::default(), } } + pub fn see(&mut self, module: &str) { + match module { + "trio" => self.seen.insert(Modules::TRIO), + "numpy" => self.seen.insert(Modules::NUMPY), + "pandas" => self.seen.insert(Modules::PANDAS), + "pytest" => self.seen.insert(Modules::PYTEST), + "django" => self.seen.insert(Modules::DJANGO), + "six" => self.seen.insert(Modules::SIX), + "logging" => self.seen.insert(Modules::LOGGING), + "typing" => self.seen.insert(Modules::TYPING), + "tarfile" => self.seen.insert(Modules::TARFILE), + "re" => self.seen.insert(Modules::RE), + "collections" => self.seen.insert(Modules::COLLECTIONS), + _ => {} + } + } + + /// Return `true` if the module at the given path was seen anywhere in the semantic model. + /// This includes both direct imports (`import trio`) and member imports (`from trio import + /// TrioTask`). + pub fn seen(&self, module: Modules) -> bool { + self.seen.contains(module) + } + /// Return the [`Binding`] for the given [`BindingId`]. #[inline] pub fn binding(&self, id: BindingId) -> &Binding { @@ -1297,16 +1325,6 @@ impl<'a> SemanticModel<'a> { exceptions } - /// Return `true` if the module at the given path was seen anywhere in the semantic model. - /// This includes both direct imports (`import trio`) and member imports (`from trio import - /// TrioTask`). - pub fn seen(&self, module: &[&str]) -> bool { - self.bindings - .iter() - .filter_map(Binding::as_any_import) - .any(|import| import.call_path().starts_with(module)) - } - /// Generate a [`Snapshot`] of the current semantic model. pub fn snapshot(&self) -> Snapshot { Snapshot { @@ -1532,6 +1550,22 @@ impl ShadowedBinding { } } +bitflags! { + pub struct Modules: u32 { + const TRIO = 1 << 0; + const DJANGO = 1 << 1; + const NUMPY = 1 << 2; + const SIX = 1 << 3; + const PANDAS = 1 << 4; + const LOGGING = 1 << 5; + const TYPING = 1 << 6; + const PYTEST = 1 << 7; + const TARFILE = 1 << 8; + const RE = 1 << 9; + const COLLECTIONS = 1 << 10; + } +} + bitflags! { /// Flags indicating the current model state. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]