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::ast::{self, AstNode, SubstringArgs};

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

use super::{ActionKind, CodeAction};

pub(super) fn rewrite_substring_as_function_call(
    db: &dyn Db,
    position: InFile<TextSize>,
    actions: &mut Vec<CodeAction>,
) -> Option<()> {
    let token = token_from_offset(db, position)?;
    let substring = token.parent_ancestors().find_map(ast::SubstringFn::cast)?;

    let arguments = match substring.substring_args()? {
        SubstringArgs::SubstringFromFor(args) => {
            let string = args.string()?;
            let start = args.start()?;
            match args.count() {
                Some(count) => format!(
                    "{}, {}, {}",
                    string.syntax().text(),
                    start.syntax().text(),
                    count.syntax().text()
                ),
                None => format!("{}, {}", string.syntax().text(), start.syntax().text()),
            }
        }
        SubstringArgs::SubstringForFrom(args) => {
            let string = args.string()?;
            let count = args.count()?;
            let start = args
                .start()
                .map(|start| start.syntax().text().to_string())
                .unwrap_or_else(|| "1".to_owned());
            format!(
                "{}, {start}, {}",
                string.syntax().text(),
                count.syntax().text()
            )
        }
        SubstringArgs::SubstringSimilarEscape(args) => format!(
            "{}, {}, {}",
            args.string()?.syntax().text(),
            args.pattern()?.syntax().text(),
            args.escape()?.syntax().text()
        ),
        SubstringArgs::SubstringExprs(_) => return None,
    };

    actions.push(CodeAction {
        title: "Rewrite as `substring` function call".to_owned(),
        edits: vec![Edit::replace(
            substring.syntax().text_range(),
            format!("substring({arguments})"),
        )],
        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::rewrite_substring_as_function_call;

    #[test]
    fn rewrites_substring_from_for_as_function_call() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select SUBS$0TRING('hello world' FROM 3 FOR 5);",
            ),
            @"select substring('hello world', 3, 5);"
        );
    }

    #[test]
    fn rewrites_for_before_from_in_function_argument_order() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(value FOR leng$0th FROM start);",
            ),
            @"select substring(value, start, length);"
        );
    }

    #[test]
    fn rewrites_optional_from_only_form() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(lower(val$0ue) FROM start);",
            ),
            @"select substring(lower(value), start);"
        );
    }

    #[test]
    fn rewrites_optional_for_only_form() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(value FOR leng$0th);",
            ),
            @"select substring(value, 1, length);"
        );
    }

    #[test]
    fn rewrites_similar_escape_form() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                r##"select SUBS$0TRING('hello world' SIMILAR '%#"o w#"%' ESCAPE '#');"##,
            ),
            @r##"select substring('hello world', '%#"o w#"%', '#');"##
        );
    }

    #[test]
    fn rewrites_similar_escape_expression_arguments() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(lower(value) similar ('%' || patt$0ern) escape coalesce(escape_char, '#'));",
            ),
            @"select substring(lower(value), ('%' || pattern), coalesce(escape_char, '#'));"
        );
    }

    #[test]
    fn rewrites_innermost_from_for_call_and_preserves_aggregate_clause() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(substring(value FR$0OM 2) FROM 3 FOR 4) FILTER (WHERE ok);",
            ),
            @"select substring(substring(value, 2) FROM 3 FOR 4) FILTER (WHERE ok);"
        );
    }

    #[test]
    fn rewrites_innermost_similar_escape_call_and_preserves_aggregate_clause() {
        assert_snapshot!(
            apply_code_action(
                rewrite_substring_as_function_call,
                "select substring(substring(value similar patt$0ern escape esc) similar outer_pattern escape outer_esc) filter (where ok);",
            ),
            @"select substring(substring(value, pattern, esc) similar outer_pattern escape outer_esc) filter (where ok);"
        );
    }

    #[test]
    fn not_applicable_to_comma_separated_call() {
        assert!(code_action_not_applicable(
            rewrite_substring_as_function_call,
            "select substring(value, 2$0, 3);"
        ));
    }
}