squawk-ide 2.63.0

Linter for Postgres migrations & SQL
Documentation
use rowan::TextSize;
use salsa::Database as Db;
use squawk_linter::Edit;
use squawk_syntax::{
    SyntaxKind,
    ast::{self, AstNode},
    quote::quote_string_literal,
};

use crate::{file::InFile, offsets::token_from_offset};

use super::{ActionKind, CodeAction};

pub(super) fn rewrite_normalize_as_function_call(
    db: &dyn Db,
    position: InFile<TextSize>,
    actions: &mut Vec<CodeAction>,
) -> Option<()> {
    let token = token_from_offset(db, position)?;
    let (name, normal_form) = token.parent_ancestors().find_map(|node| {
        let call = ast::CallExpr::cast(node)?;
        let ast::Expr::NameRef(name) = call.expr()? else {
            return None;
        };
        if name.syntax().first_token()?.kind() != SyntaxKind::NORMALIZE_KW {
            return None;
        }

        let args = call.arg_list()?.args().collect::<Vec<_>>();
        let normal_form = match args.as_slice() {
            [arg] => {
                arg.expr()?;
                None
            }
            [arg, form] => {
                arg.expr()?;
                Some(normal_form(&form.expr()?)?)
            }
            _ => return None,
        };

        Some((name, normal_form))
    })?;

    let mut edits = vec![Edit::replace(
        name.syntax().text_range(),
        "pg_catalog.normalize",
    )];
    if let Some((range, form)) = normal_form {
        edits.push(Edit::replace(range, quote_string_literal(&form)));
    }

    actions.push(CodeAction {
        title: "Rewrite as function call `pg_catalog.normalize()`".to_owned(),
        edits,
        kind: ActionKind::RefactorRewrite,
    });

    Some(())
}

fn normal_form(expr: &ast::Expr) -> Option<(rowan::TextRange, String)> {
    let ast::Expr::NameRef(name) = expr else {
        return None;
    };
    let token = name.syntax().first_token()?;
    if !matches!(
        token.kind(),
        SyntaxKind::NFC_KW | SyntaxKind::NFD_KW | SyntaxKind::NFKC_KW | SyntaxKind::NFKD_KW
    ) {
        return None;
    }
    Some((
        expr.syntax().text_range(),
        token.text().to_ascii_uppercase(),
    ))
}

#[cfg(test)]
mod test {
    use insta::assert_snapshot;

    use crate::code_actions::test_utils::{apply_code_action, code_action_not_applicable};

    use super::rewrite_normalize_as_function_call;

    #[test]
    fn rewrites_normalize_as_function_call() {
        assert_snapshot!(
            apply_code_action(
                rewrite_normalize_as_function_call,
                r"select NORM$0ALIZE(U&'\0061\0301', NFC);",
            ),
            @r"select pg_catalog.normalize(U&'\0061\0301', 'NFC');"
        );
    }

    #[test]
    fn rewrites_default_normal_form_with_cursor_in_argument() {
        assert_snapshot!(
            apply_code_action(
                rewrite_normalize_as_function_call,
                "select normalize(val$0ue);",
            ),
            @"select pg_catalog.normalize(value);"
        );
    }

    #[test]
    fn canonicalizes_normal_form_and_preserves_call_clauses() {
        assert_snapshot!(
            apply_code_action(
                rewrite_normalize_as_function_call,
                "select normalize(value, nf$0kd) FILTER (WHERE ok);",
            ),
            @"select pg_catalog.normalize(value, 'NFKD') FILTER (WHERE ok);"
        );
    }

    #[test]
    fn rewrites_innermost_normalize_call() {
        assert_snapshot!(
            apply_code_action(
                rewrite_normalize_as_function_call,
                "select normalize(normal$0ize(value, nfd), nfkc);",
            ),
            @"select normalize(pg_catalog.normalize(value, 'NFD'), nfkc);"
        );
    }

    #[test]
    fn not_applicable_to_already_qualified_call() {
        assert!(code_action_not_applicable(
            rewrite_normalize_as_function_call,
            "select pg_catalog.norm$0alize(value, 'NFC');"
        ));
    }

    #[test]
    fn not_applicable_to_non_normal_form_argument() {
        assert!(code_action_not_applicable(
            rewrite_normalize_as_function_call,
            "select normalize(value, fo$0rm);"
        ));
    }
}