draxl-patch 0.1.1

Structured semantic patch operations over the Draxl model
Documentation
use super::support::{
    is_item_trivia, is_stmt_trivia, semantic_item_target_ids, semantic_stmt_target_ids,
};
use crate::error::{patch_error, PatchError};
use crate::schema::{
    attach_target_not_sibling_message, detach_requires_following_sibling_message, find_node_kind,
    is_attachable_kind,
};
use draxl_ast::{Block, Expr, File, Item, LowerLanguage, Stmt};

pub(super) fn apply_attach(
    language: LowerLanguage,
    file: &mut File,
    node_id: &str,
    target_id: &str,
) -> Result<(), PatchError> {
    if !find_node_kind(language, file, node_id)
        .is_some_and(|kind| is_attachable_kind(language, kind))
    {
        return Err(patch_error(&format!(
            "attach source `{node_id}` was not found or is not attachable"
        )));
    }

    if attach_in_items(language, &mut file.items, node_id, target_id)? {
        Ok(())
    } else {
        Err(patch_error(&format!(
            "attach source `{node_id}` was not found or is not attachable"
        )))
    }
}

pub(super) fn apply_detach(
    language: LowerLanguage,
    file: &mut File,
    node_id: &str,
) -> Result<(), PatchError> {
    if !find_node_kind(language, file, node_id)
        .is_some_and(|kind| is_attachable_kind(language, kind))
    {
        return Err(patch_error(&format!(
            "detach source `{node_id}` was not found or is not attachable"
        )));
    }

    if detach_in_items(language, &mut file.items, node_id)? {
        Ok(())
    } else {
        Err(patch_error(&format!(
            "detach source `{node_id}` was not found or is not attachable"
        )))
    }
}

fn attach_in_items(
    language: LowerLanguage,
    items: &mut Vec<Item>,
    node_id: &str,
    target_id: &str,
) -> Result<bool, PatchError> {
    let local_targets = semantic_item_target_ids(items)
        .into_iter()
        .map(str::to_owned)
        .collect::<Vec<_>>();
    for item in items.iter_mut() {
        match item {
            Item::Doc(node) if node.meta.id == node_id => {
                if !local_targets.iter().any(|candidate| candidate == target_id) {
                    return Err(patch_error(&attach_target_not_sibling_message(
                        language, target_id, node_id,
                    )));
                }
                node.meta.anchor = Some(target_id.to_owned());
                return Ok(true);
            }
            Item::Comment(node) if node.meta.id == node_id => {
                if !local_targets.iter().any(|candidate| candidate == target_id) {
                    return Err(patch_error(&attach_target_not_sibling_message(
                        language, target_id, node_id,
                    )));
                }
                node.meta.anchor = Some(target_id.to_owned());
                return Ok(true);
            }
            _ => {}
        }
    }

    for item in items {
        if let Some(found) = recurse_item_for_attach(language, item, node_id, target_id)? {
            return Ok(found);
        }
    }

    Ok(false)
}

fn recurse_item_for_attach(
    language: LowerLanguage,
    item: &mut Item,
    node_id: &str,
    target_id: &str,
) -> Result<Option<bool>, PatchError> {
    match item {
        Item::Mod(module) => {
            attach_in_items(language, &mut module.items, node_id, target_id).map(Some)
        }
        Item::Fn(function) => {
            attach_in_stmts(language, &mut function.body.stmts, node_id, target_id).map(Some)
        }
        Item::Use(_) | Item::Struct(_) | Item::Enum(_) | Item::Doc(_) | Item::Comment(_) => {
            Ok(None)
        }
    }
}

fn attach_in_stmts(
    language: LowerLanguage,
    stmts: &mut Vec<Stmt>,
    node_id: &str,
    target_id: &str,
) -> Result<bool, PatchError> {
    let local_targets = semantic_stmt_target_ids(stmts)
        .into_iter()
        .map(str::to_owned)
        .collect::<Vec<_>>();
    for stmt in stmts.iter_mut() {
        match stmt {
            Stmt::Doc(node) if node.meta.id == node_id => {
                if !local_targets.iter().any(|candidate| candidate == target_id) {
                    return Err(patch_error(&attach_target_not_sibling_message(
                        language, target_id, node_id,
                    )));
                }
                node.meta.anchor = Some(target_id.to_owned());
                return Ok(true);
            }
            Stmt::Comment(node) if node.meta.id == node_id => {
                if !local_targets.iter().any(|candidate| candidate == target_id) {
                    return Err(patch_error(&attach_target_not_sibling_message(
                        language, target_id, node_id,
                    )));
                }
                node.meta.anchor = Some(target_id.to_owned());
                return Ok(true);
            }
            _ => {}
        }
    }

    for stmt in stmts {
        if let Some(found) = recurse_stmt_for_attach(language, stmt, node_id, target_id)? {
            return Ok(found);
        }
    }

    Ok(false)
}

fn recurse_stmt_for_attach(
    language: LowerLanguage,
    stmt: &mut Stmt,
    node_id: &str,
    target_id: &str,
) -> Result<Option<bool>, PatchError> {
    match stmt {
        Stmt::Let(let_stmt) => {
            attach_in_expr(language, &mut let_stmt.value, node_id, target_id).map(Some)
        }
        Stmt::Expr(expr_stmt) => {
            attach_in_expr(language, &mut expr_stmt.expr, node_id, target_id).map(Some)
        }
        Stmt::Item(item) => recurse_item_for_attach(language, item, node_id, target_id),
        Stmt::Doc(_) | Stmt::Comment(_) => Ok(None),
    }
}

fn attach_in_expr(
    language: LowerLanguage,
    expr: &mut Expr,
    node_id: &str,
    target_id: &str,
) -> Result<bool, PatchError> {
    match expr {
        Expr::Group(group) => attach_in_expr(language, &mut group.expr, node_id, target_id),
        Expr::Binary(binary) => {
            if attach_in_expr(language, &mut binary.lhs, node_id, target_id)? {
                return Ok(true);
            }
            attach_in_expr(language, &mut binary.rhs, node_id, target_id)
        }
        Expr::Unary(unary) => attach_in_expr(language, &mut unary.expr, node_id, target_id),
        Expr::Call(call) => {
            if attach_in_expr(language, &mut call.callee, node_id, target_id)? {
                return Ok(true);
            }
            for arg in &mut call.args {
                if attach_in_expr(language, arg, node_id, target_id)? {
                    return Ok(true);
                }
            }
            Ok(false)
        }
        Expr::Match(match_expr) => {
            if attach_in_expr(language, &mut match_expr.scrutinee, node_id, target_id)? {
                return Ok(true);
            }
            for arm in &mut match_expr.arms {
                if let Some(guard) = &mut arm.guard {
                    if attach_in_expr(language, guard, node_id, target_id)? {
                        return Ok(true);
                    }
                }
                if attach_in_expr(language, &mut arm.body, node_id, target_id)? {
                    return Ok(true);
                }
            }
            Ok(false)
        }
        Expr::Block(Block { stmts, .. }) => attach_in_stmts(language, stmts, node_id, target_id),
        Expr::Path(_) | Expr::Lit(_) => Ok(false),
    }
}

fn detach_in_items(
    language: LowerLanguage,
    items: &mut Vec<Item>,
    node_id: &str,
) -> Result<bool, PatchError> {
    for index in 0..items.len() {
        let has_following = has_following_semantic_item(items, index);
        match &mut items[index] {
            Item::Doc(node) if node.meta.id == node_id => {
                if !has_following {
                    return Err(patch_error(&detach_requires_following_sibling_message(
                        language, node_id,
                    )));
                }
                node.meta.anchor = None;
                return Ok(true);
            }
            Item::Comment(node) if node.meta.id == node_id => {
                if !has_following {
                    return Err(patch_error(&detach_requires_following_sibling_message(
                        language, node_id,
                    )));
                }
                node.meta.anchor = None;
                return Ok(true);
            }
            _ => {}
        }
    }

    for item in items {
        if let Some(found) = recurse_item_for_detach(language, item, node_id)? {
            return Ok(found);
        }
    }

    Ok(false)
}

fn recurse_item_for_detach(
    language: LowerLanguage,
    item: &mut Item,
    node_id: &str,
) -> Result<Option<bool>, PatchError> {
    match item {
        Item::Mod(module) => detach_in_items(language, &mut module.items, node_id).map(Some),
        Item::Fn(function) => {
            detach_in_stmts(language, &mut function.body.stmts, node_id).map(Some)
        }
        Item::Use(_) | Item::Struct(_) | Item::Enum(_) | Item::Doc(_) | Item::Comment(_) => {
            Ok(None)
        }
    }
}

fn detach_in_stmts(
    language: LowerLanguage,
    stmts: &mut Vec<Stmt>,
    node_id: &str,
) -> Result<bool, PatchError> {
    for index in 0..stmts.len() {
        let has_following = has_following_semantic_stmt(stmts, index);
        match &mut stmts[index] {
            Stmt::Doc(node) if node.meta.id == node_id => {
                if !has_following {
                    return Err(patch_error(&detach_requires_following_sibling_message(
                        language, node_id,
                    )));
                }
                node.meta.anchor = None;
                return Ok(true);
            }
            Stmt::Comment(node) if node.meta.id == node_id => {
                if !has_following {
                    return Err(patch_error(&detach_requires_following_sibling_message(
                        language, node_id,
                    )));
                }
                node.meta.anchor = None;
                return Ok(true);
            }
            _ => {}
        }
    }

    for stmt in stmts {
        if let Some(found) = recurse_stmt_for_detach(language, stmt, node_id)? {
            return Ok(found);
        }
    }

    Ok(false)
}

fn recurse_stmt_for_detach(
    language: LowerLanguage,
    stmt: &mut Stmt,
    node_id: &str,
) -> Result<Option<bool>, PatchError> {
    match stmt {
        Stmt::Let(let_stmt) => detach_in_expr(language, &mut let_stmt.value, node_id).map(Some),
        Stmt::Expr(expr_stmt) => detach_in_expr(language, &mut expr_stmt.expr, node_id).map(Some),
        Stmt::Item(item) => recurse_item_for_detach(language, item, node_id),
        Stmt::Doc(_) | Stmt::Comment(_) => Ok(None),
    }
}

fn detach_in_expr(
    language: LowerLanguage,
    expr: &mut Expr,
    node_id: &str,
) -> Result<bool, PatchError> {
    match expr {
        Expr::Group(group) => detach_in_expr(language, &mut group.expr, node_id),
        Expr::Binary(binary) => {
            if detach_in_expr(language, &mut binary.lhs, node_id)? {
                return Ok(true);
            }
            detach_in_expr(language, &mut binary.rhs, node_id)
        }
        Expr::Unary(unary) => detach_in_expr(language, &mut unary.expr, node_id),
        Expr::Call(call) => {
            if detach_in_expr(language, &mut call.callee, node_id)? {
                return Ok(true);
            }
            for arg in &mut call.args {
                if detach_in_expr(language, arg, node_id)? {
                    return Ok(true);
                }
            }
            Ok(false)
        }
        Expr::Match(match_expr) => {
            if detach_in_expr(language, &mut match_expr.scrutinee, node_id)? {
                return Ok(true);
            }
            for arm in &mut match_expr.arms {
                if let Some(guard) = &mut arm.guard {
                    if detach_in_expr(language, guard, node_id)? {
                        return Ok(true);
                    }
                }
                if detach_in_expr(language, &mut arm.body, node_id)? {
                    return Ok(true);
                }
            }
            Ok(false)
        }
        Expr::Block(Block { stmts, .. }) => detach_in_stmts(language, stmts, node_id),
        Expr::Path(_) | Expr::Lit(_) => Ok(false),
    }
}

fn has_following_semantic_item(items: &[Item], index: usize) -> bool {
    items[index + 1..].iter().any(|item| !is_item_trivia(item))
}

fn has_following_semantic_stmt(stmts: &[Stmt], index: usize) -> bool {
    stmts[index + 1..].iter().any(|stmt| !is_stmt_trivia(stmt))
}