draxl-merge 0.1.1

Merge analysis for Draxl patch streams.
Documentation
use crate::context::TreeContext;
use crate::model::{ConflictOwner, ConflictRegion};
use draxl_ast::{File, LowerLanguage};
use draxl_patch::{PatchDest, PatchNode, PatchOp, PatchValue, SlotOwner, SlotRef};
use draxl_rust::{
    extract_semantic_changes as extract_rust_semantic_changes, SemanticOp, SemanticPatchNode,
    SemanticSlotOwner, SemanticSlotRef,
};
pub(crate) use draxl_rust::{SemanticChange, SemanticOwner, SemanticRegion};

impl From<&SemanticOwner> for ConflictOwner {
    fn from(owner: &SemanticOwner) -> Self {
        match owner {
            SemanticOwner::Binding { let_id, binding_id } => Self::Binding {
                let_id: let_id.clone(),
                binding_id: binding_id.clone(),
            },
            SemanticOwner::Parameter {
                fn_id,
                param_id,
                param_name,
            } => Self::Parameter {
                fn_id: fn_id.clone(),
                param_id: param_id.clone(),
                param_name: param_name.clone(),
            },
        }
    }
}

impl From<SemanticRegion> for ConflictRegion {
    fn from(region: SemanticRegion) -> Self {
        match region {
            SemanticRegion::BindingName => Self::BindingName,
            SemanticRegion::BindingInitializer => Self::BindingInitializer,
            SemanticRegion::ParameterTypeContract => Self::ParameterTypeContract,
            SemanticRegion::ParameterBodyInterpretation => Self::ParameterBodyInterpretation,
        }
    }
}

pub(crate) fn extract_semantic_changes(
    ops: &[PatchOp],
    context: &TreeContext,
) -> Vec<SemanticChange> {
    let semantic_ops = ops.iter().map(translate_op).collect::<Vec<_>>();
    extract_rust_semantic_changes(&semantic_ops, context)
}

pub(crate) fn extract_semantic_changes_for_language(
    language: LowerLanguage,
    base: &File,
    ops: &[PatchOp],
) -> Vec<SemanticChange> {
    match language {
        LowerLanguage::Rust => extract_semantic_changes(ops, &TreeContext::build(base)),
    }
}

fn translate_op(op: &PatchOp) -> SemanticOp {
    match op {
        PatchOp::Insert { .. } | PatchOp::Attach { .. } | PatchOp::Detach { .. } => {
            SemanticOp::Other
        }
        PatchOp::Put { slot, node } => SemanticOp::Put {
            slot: translate_slot_ref(slot),
            node: translate_patch_node(node),
        },
        PatchOp::Replace {
            target_id,
            replacement,
        } => SemanticOp::Replace {
            target_id: target_id.clone(),
            replacement: translate_patch_node(replacement),
        },
        PatchOp::Delete { target_id } => SemanticOp::Delete {
            target_id: target_id.clone(),
        },
        PatchOp::Move { target_id, dest } => SemanticOp::Move {
            target_id: target_id.clone(),
            dest_slot: match dest {
                PatchDest::Ranked(_) => None,
                PatchDest::Slot(slot) => Some(translate_slot_ref(slot)),
            },
        },
        PatchOp::Set { path, value } => SemanticOp::Set {
            node_id: path.node_id.clone(),
            path: path.segments.clone(),
            ident_value: matches!(value, PatchValue::Ident(_)),
        },
        PatchOp::Clear { path } => SemanticOp::Clear {
            node_id: path.node_id.clone(),
        },
    }
}

fn translate_slot_ref(slot: &SlotRef) -> SemanticSlotRef {
    SemanticSlotRef {
        owner: match &slot.owner {
            SlotOwner::File => SemanticSlotOwner::File,
            SlotOwner::Node(owner_id) => SemanticSlotOwner::Node(owner_id.clone()),
        },
        slot: slot.slot.clone(),
    }
}

fn translate_patch_node(node: &PatchNode) -> Option<SemanticPatchNode> {
    match node {
        PatchNode::Expr(expr) => Some(SemanticPatchNode::Expr(expr.clone())),
        PatchNode::Stmt(stmt) => Some(SemanticPatchNode::Stmt(stmt.clone())),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::{extract_semantic_changes, SemanticChange, SemanticOwner, SemanticRegion};
    use crate::context::TreeContext;
    use draxl_patch::{PatchNode, PatchOp, PatchPath, PatchValue, SlotOwner, SlotRef};
    use draxl_validate::validate_file;

    #[test]
    fn extracts_binding_name_changes() {
        let file = parse_source(
            r#"
@m1 mod demo {
  @f1[a] fn price(@p1[a] amount: @t1 Cents) -> @t2 Cents {
    @s1[a] let @p2 subtotal = @e1 amount;
    @s2[b] @e2 subtotal
  }
}
"#,
        );
        let ops = vec![PatchOp::Set {
            path: PatchPath {
                node_id: "p2".to_owned(),
                segments: vec!["name".to_owned()],
            },
            value: PatchValue::Ident("subtotal_cents".to_owned()),
        }];

        let changes = extract_semantic_changes(&ops, &TreeContext::build(&file));

        assert_eq!(
            changes,
            vec![SemanticChange {
                owner: SemanticOwner::Binding {
                    let_id: "s1".to_owned(),
                    binding_id: "p2".to_owned(),
                },
                region: SemanticRegion::BindingName,
                op_index: 0,
            }]
        );
    }

    #[test]
    fn extracts_binding_initializer_changes() {
        let file = parse_source(
            r#"
@m1 mod demo {
  @f1[a] fn price(@p1[a] amount: @t1 Cents) -> @t2 Cents {
    @s1[a] let @p2 subtotal = @e1 amount;
    @s2[b] @e2 subtotal
  }
}
"#,
        );
        let ops = vec![PatchOp::Replace {
            target_id: "e1".to_owned(),
            replacement: PatchNode::Expr(
                draxl_parser::parse_expr_fragment("@e1 zero_dollars()")
                    .expect("initializer replacement should parse"),
            ),
        }];

        let changes = extract_semantic_changes(&ops, &TreeContext::build(&file));

        assert_eq!(
            changes,
            vec![SemanticChange {
                owner: SemanticOwner::Binding {
                    let_id: "s1".to_owned(),
                    binding_id: "p2".to_owned(),
                },
                region: SemanticRegion::BindingInitializer,
                op_index: 0,
            }]
        );
    }

    #[test]
    fn extracts_parameter_type_contract_changes() {
        let file = parse_source(
            r#"
@m1 mod demo {
  @f1[a] fn is_discount_allowed(@p1[a] rate: @t1 Percent) -> @t2 bool {
    @s1[a] @e1 (@e2 rate < @l1 100)
  }
}
"#,
        );
        let ops = vec![PatchOp::Put {
            slot: SlotRef {
                owner: SlotOwner::Node("p1".to_owned()),
                slot: "ty".to_owned(),
            },
            node: PatchNode::Type(
                draxl_parser::parse_type_fragment("@t3 BasisPoints")
                    .expect("parameter type replacement fragment should parse"),
            ),
        }];

        let changes = extract_semantic_changes(&ops, &TreeContext::build(&file));

        assert_eq!(
            changes,
            vec![SemanticChange {
                owner: SemanticOwner::Parameter {
                    fn_id: "f1".to_owned(),
                    param_id: "p1".to_owned(),
                    param_name: "rate".to_owned(),
                },
                region: SemanticRegion::ParameterTypeContract,
                op_index: 0,
            }]
        );
    }

    #[test]
    fn extracts_parameter_body_interpretation_changes() {
        let file = parse_source(
            r#"
@m1 mod demo {
  @f1[a] fn is_discount_allowed(@p1[a] rate: @t1 Percent, @p2[b] other: @t3 Percent) -> @t2 bool {
    @s1[a] @e1 (@e2 rate < @l1 100)
  }
}
"#,
        );
        let ops = vec![PatchOp::Replace {
            target_id: "e1".to_owned(),
            replacement: PatchNode::Expr(
                draxl_parser::parse_expr_fragment("@e1 (@e2 rate < @l1 95)")
                    .expect("body replacement fragment should parse"),
            ),
        }];

        let changes = extract_semantic_changes(&ops, &TreeContext::build(&file));

        assert_eq!(
            changes,
            vec![SemanticChange {
                owner: SemanticOwner::Parameter {
                    fn_id: "f1".to_owned(),
                    param_id: "p1".to_owned(),
                    param_name: "rate".to_owned(),
                },
                region: SemanticRegion::ParameterBodyInterpretation,
                op_index: 0,
            }]
        );
    }

    fn parse_source(source: &str) -> draxl_ast::File {
        let file = draxl_parser::parse_file(source).expect("source should parse");
        validate_file(&file).expect("source should validate");
        file
    }
}