Repository: astral-sh/ruff
File: crates/ruff_linter/src/rules/airflow/rules/function_signature_change_in_3.rs (Rust)

Code before:

    fn fix_title(&self) -> Option<String> {
        let Airflow3FunctionSignatureChange { change_type, .. } = self;
        match change_type {
            FunctionSignatureChangeType::KeywordOnly { message } => Some(message.to_string()),
        }
    }
}

/// AIR303
pub(crate) fn airflow_3_function_signature_change_expr(checker: &Checker, expr: &Expr) {
    if !checker.semantic().seen_module(Modules::AIRFLOW) {
        return;
    }

    airflow_3_keyword_args_only_function(checker, expr);
}

/// Check for functions that changed to only accept keyword arguments
fn airflow_3_keyword_args_only_function(checker: &Checker, expr: &Expr) {
    if let Expr::Call(call_expr @ ExprCall { arguments, .. }) = expr {
        check_keyword_only_method(checker, call_expr, arguments);
    }
}

fn check_keyword_only_method(checker: &Checker, call_expr: &ExprCall, arguments: &Arguments) {
    let Expr::Attribute(ExprAttribute { attr, value, .. }) = &*call_expr.func else {
        return;
    };

    let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) else {
        return;
    };

    let change_type = match qualname.segments() {
        ["airflow", "lineage", "hook", "HookLineageCollector"] => match attr.as_str() {
            "create_asset" => {
                if arguments.find_positional(0).is_some() {
                    FunctionSignatureChangeType::KeywordOnly {
                        message: "Pass positional arguments as keyword arguments (e.g., `create_asset(uri=...)`)",
                    }
                } else {
                    // No positional args, no violation
                    return;
                }
            }
            _ => return,
        },
        _ => return,
    };


Review comment (suggestion):
nit: I think I would probably just inline these functions:


```suggestion
    let Expr::Call(call_expr @ ExprCall { arguments, .. }) = expr else {
        return;
    }

```

Code after:

    fn fix_title(&self) -> Option<String> {
        let Airflow3IncompatibleFunctionSignature { change_type, .. } = self;
        match change_type {
            FunctionSignatureChangeType::KeywordOnly { message } => Some(message.to_string()),
        }
    }
}

/// AIR303
pub(crate) fn airflow_3_incompatible_function_signature(checker: &Checker, expr: &Expr) {
    if !checker.semantic().seen_module(Modules::AIRFLOW) {
        return;
    }

    let Expr::Call(ExprCall {
        func, arguments, ..
    }) = expr
    else {
        return;
    };

    let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
        return;
    };

    // Resolve the qualified name: try variable assignments first, then fall back to direct
    // constructor calls.
    let qualified_name = typing::resolve_assignment(value, checker.semantic()).or_else(|| {
        value
            .as_call_expr()
            .and_then(|call| checker.semantic().resolve_qualified_name(&call.func))
    });

    let Some(qualified_name) = qualified_name else {
        return;
    };

    check_keyword_only_method(checker, &qualified_name, attr, arguments);
}

fn check_keyword_only_method(
    checker: &Checker,
    qualified_name: &QualifiedName,
    attr: &Identifier,
    arguments: &Arguments,
) {
    let has_positional_args =
        arguments.find_positional(0).is_some() || arguments.args.iter().any(Expr::is_starred_expr);

    if let ["airflow", "lineage", "hook", "HookLineageCollector"] = qualified_name.segments() {