use crate::checkers::ast::Checker;
use crate::fix::edits::remove_unused_imports;
use crate::importer::ImportRequest;
use crate::rules::numpy::helpers::{AttributeSearcher, ImportSearcher};
use ruff_diagnostics::{Edit, Fix};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{Expr, ExprAttribute, ExprName, StmtFunctionDef, StmtTry};
use ruff_python_semantic::Exceptions;
use ruff_python_semantic::ScopeKind;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::class::any_qualified_base_class;
use ruff_python_semantic::{MemberNameImport, NameImport};
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
pub(crate) const INTERNAL_MODULE_WARNING: &str = "This is an internal module which is not suggested to be used and is subject to change without notice.";
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Replacement {
None,
Message(&'static str),
AttrName(&'static str),
Rename {
module: &'static str,
name: &'static str,
},
SourceModuleMoved {
module: &'static str,
name: String,
},
SourceModuleMovedToSDK {
module: &'static str,
name: String,
version: &'static str,
},
SourceModuleMovedWithMessage {
module: &'static str,
name: String,
message: &'static str,
suggest_fix: bool,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProviderReplacement {
Rename {
module: &'static str,
name: &'static str,
provider: &'static str,
version: &'static str,
},
SourceModuleMovedToProvider {
module: &'static str,
name: String,
provider: &'static str,
version: &'static str,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum FunctionSignatureChange {
Message(&'static str),
}
pub(crate) fn is_guarded_by_try_except(
expr: &Expr,
module: &str,
name: &str,
semantic: &SemanticModel,
) -> bool {
match expr {
Expr::Attribute(_) => {
if !semantic.in_exception_handler() {
return false;
}
let Some(try_node) = semantic
.current_statements()
.find_map(|stmt| stmt.as_try_stmt())
else {
return false;
};
let suspended_exceptions = Exceptions::from_try_stmt(try_node, semantic);
if !suspended_exceptions.contains(Exceptions::ATTRIBUTE_ERROR) {
return false;
}
try_block_contains_undeprecated_attribute(try_node, module, name, semantic)
}
Expr::Name(ExprName { id, .. }) => {
let Some(binding_id) = semantic.lookup_symbol(id.as_str()).binding_id() else {
return false;
};
let binding = semantic.binding(binding_id);
if !binding.is_external() {
return false;
}
if !binding.in_exception_handler() {
return false;
}
let Some(try_node) = binding.source.and_then(|import_id| {
semantic
.statements(import_id)
.find_map(|stmt| stmt.as_try_stmt())
}) else {
return false;
};
let suspended_exceptions = Exceptions::from_try_stmt(try_node, semantic);
if !suspended_exceptions
.intersects(Exceptions::IMPORT_ERROR | Exceptions::MODULE_NOT_FOUND_ERROR)
{
return false;
}
try_block_contains_undeprecated_import(try_node, module, name)
}
_ => false,
}
}
fn try_block_contains_undeprecated_attribute(
try_node: &StmtTry,
module: &str,
name: &str,
semantic: &SemanticModel,
) -> bool {
let undeprecated_qualified_name = {
let mut builder = QualifiedNameBuilder::default();
for part in module.split('.') {
builder.push(part);
}
builder.push(name);
builder.build()
};
let mut attribute_searcher = AttributeSearcher::new(undeprecated_qualified_name, semantic);
attribute_searcher.visit_body(&try_node.body);
attribute_searcher.found_attribute
}
fn try_block_contains_undeprecated_import(try_node: &StmtTry, module: &str, name: &str) -> bool {
let mut import_searcher = ImportSearcher::new(module, name);
import_searcher.visit_body(&try_node.body);
import_searcher.found_import
}
pub(crate) fn is_airflow_builtin_or_provider(
segments: &[&str],
module: &str,
symbol_suffix: &str,
) -> bool {
match segments {
["airflow", "providers", rest @ ..] => {
if let (Some(pos), Some(last_element)) =
(rest.iter().position(|&s| s == module), rest.last())
{
pos + 1 < rest.len() && last_element.ends_with(symbol_suffix)
} else {
false
}
}
["airflow", first, rest @ ..] => {
if let Some(last) = rest.last() {
*first == module && last.ends_with(symbol_suffix)
} else {
false
}
}
_ => false,
}
}
fn match_head(value: &Expr) -> Option<&ExprName> {
match value {
Expr::Attribute(ExprAttribute { value, .. }) => value.as_name_expr(),
Expr::Name(name) => Some(name),
_ => None,
}
}
pub(crate) fn generate_import_edit(
expr: &Expr,
checker: &Checker,
module: &str,
name: &str,
ranged: TextRange,
) -> Option<Fix> {
let (import_edit, _) = checker
.importer()
.get_or_import_symbol(
&ImportRequest::import_from(module, name),
expr.start(),
checker.semantic(),
)
.ok()?;
let replacement_edit = Edit::range_replacement(name.to_string(), ranged.range());
Some(Fix::safe_edits(import_edit, [replacement_edit]))
}
pub(crate) fn generate_remove_and_runtime_import_edit(
expr: &Expr,
checker: &Checker,
module: &str,
name: &str,
) -> Option<Fix> {
let head = match_head(expr)?;
let semantic = checker.semantic();
let binding = semantic
.resolve_name(head)
.or_else(|| checker.semantic().lookup_symbol(&head.id).binding_id())
.map(|id| checker.semantic().binding(id))?;
let stmt = binding.statement(semantic)?;
let remove_edit = remove_unused_imports(
std::iter::once(name),
stmt,
None,
checker.locator(),
checker.stylist(),
checker.indexer(),
)
.ok()?;
let import_edit = checker.importer().add_import(
&NameImport::ImportFrom(MemberNameImport::member(
(*module).to_string(),
name.to_string(),
)),
expr.start(),
);
Some(Fix::unsafe_edits(remove_edit, [import_edit]))
}
pub(crate) fn is_method_in_subclass<F>(
function_def: &StmtFunctionDef,
semantic: &SemanticModel,
method_name: &str,
is_base_class: F,
) -> bool
where
F: Fn(QualifiedName) -> bool,
{
if function_def.name.as_str() != method_name {
return false;
}
let ScopeKind::Class(class_def) = semantic.current_scope().kind else {
return false;
};
any_qualified_base_class(class_def, semantic, &is_base_class)
}
pub(crate) fn in_airflow_task_function(semantic: &SemanticModel) -> bool {
semantic
.current_statements()
.find_map(|stmt| stmt.as_function_def_stmt())
.is_some_and(|function_def| is_airflow_task(function_def, semantic))
}
pub(crate) fn is_airflow_task(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool {
function_def.decorator_list.iter().any(|decorator| {
let expr = map_callable(&decorator.expression);
if semantic
.resolve_qualified_name(expr)
.is_some_and(|qn| matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"]))
{
return true;
}
if let Expr::Attribute(ExprAttribute { value, .. }) = expr {
return semantic.resolve_qualified_name(value).is_some_and(|qn| {
matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"])
});
}
false
})
}
pub(crate) fn is_airflow_task_variant(
function_def: &StmtFunctionDef,
semantic: &SemanticModel,
variant: &str,
) -> bool {
function_def.decorator_list.iter().any(|decorator| {
let expr = map_callable(&decorator.expression);
if let Expr::Attribute(ExprAttribute { value, attr, .. }) = expr {
attr.as_str() == variant
&& semantic.resolve_qualified_name(value).is_some_and(|qn| {
matches!(qn.segments(), ["airflow", "decorators" | "sdk", "task"])
})
} else {
false
}
})
}