squawk-ide 2.55.0

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

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

use super::{ActionKind, CodeAction};

pub(super) fn quote_identifier(
    db: &dyn Db,
    position: InFile<TextSize>,
    actions: &mut Vec<CodeAction>,
) -> Option<()> {
    let token = token_from_offset(db, position)?;
    let parent = token.parent()?;

    let (is_quoted, text, text_range) = if let Some(name) = ast::Name::cast(parent.clone()) {
        (name.is_quoted(), name.text(), name.syntax().text_range())
    } else if let Some(name_ref) = ast::NameRef::cast(parent.clone()) {
        (
            name_ref.is_quoted(),
            name_ref.text(),
            name_ref.syntax().text_range(),
        )
    } else {
        return None;
    };

    if is_quoted {
        return None;
    }

    let quoted = format!(r#""{text}""#);

    actions.push(CodeAction {
        title: "Quote identifier".to_owned(),
        edits: vec![Edit::replace(text_range, quoted)],
        kind: ActionKind::RefactorRewrite,
    });

    Some(())
}

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

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

    use super::quote_identifier;

    #[test]
    fn quote_identifier_on_name_ref() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "select x$0 from t;"),
            @r#"select "x" from t;"#
        );
    }

    #[test]
    fn quote_doesnt_show_up_for_already_quoted_ident() {
        assert!(code_action_not_applicable(
            quote_identifier,
            r#"create table T("X"$0 int);"#
        ));
    }

    #[test]
    fn quote_identifier_on_name() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "create table T(X$0 int);"),
            @r#"create table T("x" int);"#
        );
    }

    #[test]
    fn quote_identifier_lowercases() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "create table T(COL$0 int);"),
            @r#"create table T("col" int);"#
        );
    }

    #[test]
    fn quote_identifier_not_applicable_when_already_quoted() {
        assert!(code_action_not_applicable(
            quote_identifier,
            r#"select "x"$0 from t;"#
        ));
    }

    #[test]
    fn quote_identifier_not_applicable_on_select_keyword() {
        assert!(code_action_not_applicable(
            quote_identifier,
            "sel$0ect x from t;"
        ));
    }

    #[test]
    fn quote_identifier_on_keyword_column_name() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "select te$0xt from t;"),
            @r#"select "text" from t;"#
        );
    }

    #[test]
    fn quote_identifier_example_select() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "select x$0 from t;"),
            @r#"select "x" from t;"#
        );
    }

    #[test]
    fn quote_identifier_example_create_table() {
        assert_snapshot!(apply_code_action(
            quote_identifier,
            "create table T(X$0 int);"),
            @r#"create table T("x" int);"#
        );
    }
}