lisette-semantics 0.2.13

Little language inspired by Rust that compiles to Go
Documentation
use diagnostics::LisetteDiagnostic;
use syntax::ast::{Expression, MatchArm, MatchOrigin, Pattern, Span};
use syntax::types::unqualified_name;

pub fn check_verbose_failure_propagation(
    expression: &Expression,
    diagnostics: &mut Vec<LisetteDiagnostic>,
) {
    let Expression::Match {
        subject,
        arms,
        origin,
        span,
        ..
    } = expression
    else {
        return;
    };

    if arms.len() != 2 || arms.iter().any(MatchArm::has_guard) {
        return;
    }

    let subject_ty = subject.get_type();
    let fires = if subject_ty.is_option() {
        check_option_propagation(&arms[0], &arms[1])
    } else if subject_ty.is_result() {
        check_result_propagation(&arms[0], &arms[1])
    } else {
        false
    };

    if fires {
        let keyword_len = match origin {
            MatchOrigin::Explicit => 5,
            MatchOrigin::IfLet { .. } => 2,
        };
        let keyword_span = Span::new(span.file_id, span.byte_offset, keyword_len);
        diagnostics.push(diagnostics::lint::verbose_failure_propagation(
            &keyword_span,
        ));
    }
}

fn check_option_propagation(arm_a: &MatchArm, arm_b: &MatchArm) -> bool {
    let try_pair = |some_arm: &MatchArm, fail_arm: &MatchArm| {
        let Some(name) = enum_variant_binding(&some_arm.pattern, "Some") else {
            return false;
        };
        is_none_or_wildcard(&fail_arm.pattern)
            && body_is_identifier(&some_arm.expression, &name)
            && body_is_return_none(&fail_arm.expression)
    };
    try_pair(arm_a, arm_b) || try_pair(arm_b, arm_a)
}

fn check_result_propagation(arm_a: &MatchArm, arm_b: &MatchArm) -> bool {
    let try_pair = |ok_arm: &MatchArm, err_arm: &MatchArm| {
        let Some(ok_name) = enum_variant_binding(&ok_arm.pattern, "Ok") else {
            return false;
        };
        let Some(err_name) = enum_variant_binding(&err_arm.pattern, "Err") else {
            return false;
        };
        body_is_identifier(&ok_arm.expression, &ok_name)
            && body_is_return_err(&err_arm.expression, &err_name)
    };
    try_pair(arm_a, arm_b) || try_pair(arm_b, arm_a)
}

fn enum_variant_binding(pattern: &Pattern, variant: &str) -> Option<String> {
    let Pattern::EnumVariant {
        identifier,
        fields,
        rest,
        ..
    } = pattern
    else {
        return None;
    };
    if unqualified_name(identifier) != variant || *rest || fields.len() != 1 {
        return None;
    }
    let Pattern::Identifier {
        identifier: name, ..
    } = &fields[0]
    else {
        return None;
    };
    Some(name.to_string())
}

fn is_none_or_wildcard(pattern: &Pattern) -> bool {
    match pattern {
        Pattern::WildCard { .. } => true,
        Pattern::EnumVariant {
            identifier,
            fields,
            rest,
            ..
        } => unqualified_name(identifier) == "None" && fields.is_empty() && !*rest,
        _ => false,
    }
}

fn body_is_identifier(expression: &Expression, name: &str) -> bool {
    match expression.unwrap_parens() {
        Expression::Identifier { value, .. } => value.as_str() == name,
        Expression::Block { items, .. } => items.len() == 1 && body_is_identifier(&items[0], name),
        _ => false,
    }
}

fn body_is_return_none(expression: &Expression) -> bool {
    match expression.unwrap_parens() {
        Expression::Return {
            expression: inner, ..
        } => matches!(inner.unwrap_parens(), Expression::Identifier { value, .. }
            if value.as_str() == "None"),
        Expression::Block { items, .. } => items.len() == 1 && body_is_return_none(&items[0]),
        _ => false,
    }
}

fn body_is_return_err(expression: &Expression, binding: &str) -> bool {
    match expression.unwrap_parens() {
        Expression::Return {
            expression: inner, ..
        } => is_err_of_binding(inner, binding),
        Expression::Block { items, .. } => {
            items.len() == 1 && body_is_return_err(&items[0], binding)
        }
        _ => false,
    }
}

fn is_err_of_binding(expression: &Expression, binding: &str) -> bool {
    let Expression::Call {
        expression: callee,
        args,
        ..
    } = expression.unwrap_parens()
    else {
        return false;
    };
    if args.len() != 1 {
        return false;
    }
    let Expression::Identifier { value, .. } = callee.unwrap_parens() else {
        return false;
    };
    if unqualified_name(value) != "Err" {
        return false;
    }
    matches!(args[0].unwrap_parens(), Expression::Identifier { value, .. }
        if value.as_str() == binding)
}