use crate::explain::{
binding_rename_vs_initializer_change_conflict, non_convergent_replay_conflict,
parameter_type_vs_body_interpretation_change_conflict, replay_failure_conflict,
same_node_write_conflict, same_ranked_position_conflict, same_scalar_path_write_conflict,
same_single_slot_write_conflict,
};
use crate::model::{Conflict, ConflictReport, ReplayFailure, ReplayOrder, ReplayStage};
use crate::semantic::{
extract_semantic_changes_for_language, SemanticChange, SemanticOwner, SemanticRegion,
};
use draxl_ast::{File, LowerLanguage};
use draxl_patch::{apply_op_for_language, PatchDest, PatchOp, RankedDest, SlotRef};
use draxl_printer::canonicalize_file;
use draxl_validate::validate_file;
pub fn check_conflicts(base: &File, left: &[PatchOp], right: &[PatchOp]) -> ConflictReport {
check_conflicts_for_language(LowerLanguage::Rust, base, left, right)
}
pub fn check_conflicts_for_language(
language: LowerLanguage,
base: &File,
left: &[PatchOp],
right: &[PatchOp],
) -> ConflictReport {
let hard = check_hard_conflicts_for_language(language, base, left, right);
if hard.has_conflicts() {
return hard;
}
ConflictReport {
conflicts: classify_semantic_conflicts(language, base, left, right),
}
}
pub fn check_hard_conflicts(base: &File, left: &[PatchOp], right: &[PatchOp]) -> ConflictReport {
check_hard_conflicts_for_language(LowerLanguage::Rust, base, left, right)
}
pub fn check_hard_conflicts_for_language(
language: LowerLanguage,
base: &File,
left: &[PatchOp],
right: &[PatchOp],
) -> ConflictReport {
let left_then_right = replay(language, base, ReplayOrder::LeftThenRight, left, right);
let right_then_left = replay(language, base, ReplayOrder::RightThenLeft, right, left);
if let (Ok(left_then_right), Ok(right_then_left)) = (&left_then_right, &right_then_left) {
if canonicalize_file(left_then_right).without_spans()
== canonicalize_file(right_then_left).without_spans()
{
return ConflictReport::default();
}
}
let mut conflicts = classify_pairwise_conflicts(left, right);
if conflicts.is_empty() {
match (&left_then_right, &right_then_left) {
(Ok(_), Ok(_)) => conflicts.push(non_convergent_replay_conflict(left, right)),
(Err(failure), Ok(_)) | (Ok(_), Err(failure)) => {
conflicts.push(replay_failure_conflict(failure, left, right));
}
(Err(left_failure), Err(right_failure)) => {
conflicts.push(replay_failure_conflict(left_failure, left, right));
if left_failure != right_failure {
conflicts.push(replay_failure_conflict(right_failure, left, right));
}
}
}
}
ConflictReport { conflicts }
}
fn classify_pairwise_conflicts(left: &[PatchOp], right: &[PatchOp]) -> Vec<Conflict> {
let mut conflicts = Vec::new();
for (left_index, left_op) in left.iter().enumerate() {
for (right_index, right_op) in right.iter().enumerate() {
if let Some(dest) = same_ranked_dest(left_op, right_op) {
conflicts.push(same_ranked_position_conflict(
left_index,
left_op,
right_index,
right_op,
&dest,
));
continue;
}
if let Some(slot) = same_single_slot_dest(left_op, right_op) {
conflicts.push(same_single_slot_write_conflict(
left_index,
left_op,
right_index,
right_op,
&slot,
));
continue;
}
if let Some((node_id, segments)) = same_scalar_path(left_op, right_op) {
conflicts.push(same_scalar_path_write_conflict(
left_index,
left_op,
right_index,
right_op,
&node_id,
&segments,
));
continue;
}
if let Some(node_id) = same_node_target(left_op, right_op) {
conflicts.push(same_node_write_conflict(
left_index,
left_op,
right_index,
right_op,
&node_id,
));
}
}
}
conflicts
}
fn classify_semantic_conflicts(
language: LowerLanguage,
base: &File,
left: &[PatchOp],
right: &[PatchOp],
) -> Vec<Conflict> {
let left_changes = extract_semantic_changes_for_language(language, base, left);
let right_changes = extract_semantic_changes_for_language(language, base, right);
let mut conflicts = Vec::new();
for left_change in &left_changes {
for right_change in &right_changes {
if let Some(conflict) =
semantic_conflict_for_pair(left_change, left, right_change, right)
{
conflicts.push(conflict);
}
}
}
conflicts
}
fn semantic_conflict_for_pair(
left_change: &SemanticChange,
left_ops: &[PatchOp],
right_change: &SemanticChange,
right_ops: &[PatchOp],
) -> Option<Conflict> {
if left_change.owner != right_change.owner {
return None;
}
let left_op = left_ops.get(left_change.op_index)?;
let right_op = right_ops.get(right_change.op_index)?;
match (&left_change.owner, left_change.region, right_change.region) {
(
SemanticOwner::Binding { let_id, binding_id },
SemanticRegion::BindingName,
SemanticRegion::BindingInitializer,
)
| (
SemanticOwner::Binding { let_id, binding_id },
SemanticRegion::BindingInitializer,
SemanticRegion::BindingName,
) => Some(binding_rename_vs_initializer_change_conflict(
left_change.op_index,
left_op,
left_change.region.into(),
right_change.op_index,
right_op,
right_change.region.into(),
let_id,
binding_id,
)),
(
SemanticOwner::Parameter {
fn_id,
param_id,
param_name,
},
SemanticRegion::ParameterTypeContract,
SemanticRegion::ParameterBodyInterpretation,
)
| (
SemanticOwner::Parameter {
fn_id,
param_id,
param_name,
},
SemanticRegion::ParameterBodyInterpretation,
SemanticRegion::ParameterTypeContract,
) => Some(parameter_type_vs_body_interpretation_change_conflict(
left_change.op_index,
left_op,
left_change.region.into(),
right_change.op_index,
right_op,
right_change.region.into(),
fn_id,
param_id,
param_name,
)),
_ => None,
}
}
fn replay(
language: LowerLanguage,
base: &File,
order: ReplayOrder,
first: &[PatchOp],
second: &[PatchOp],
) -> Result<File, ReplayFailure> {
let mut file = base.clone();
apply_sequence(language, &mut file, order, first, first_stage)?;
apply_sequence(language, &mut file, order, second, second_stage)?;
validate_file(&file).map_err(|errors| ReplayFailure {
order,
stage: ReplayStage::Validation,
message: format_validation_errors(&errors),
})?;
Ok(file)
}
fn apply_sequence(
language: LowerLanguage,
file: &mut File,
order: ReplayOrder,
ops: &[PatchOp],
stage_for_index: impl Fn(usize) -> ReplayStage,
) -> Result<(), ReplayFailure> {
for (index, op) in ops.iter().cloned().enumerate() {
apply_op_for_language(language, file, op).map_err(|error| ReplayFailure {
order,
stage: stage_for_index(index),
message: error.to_string(),
})?;
}
Ok(())
}
fn same_node_target(left: &PatchOp, right: &PatchOp) -> Option<String> {
match (node_target(left), node_target(right)) {
(Some(left), Some(right)) if left == right => Some(left.to_owned()),
_ => None,
}
}
fn same_scalar_path(left: &PatchOp, right: &PatchOp) -> Option<(String, Vec<String>)> {
match (path_target(left), path_target(right)) {
(Some((left_node, left_segments)), Some((right_node, right_segments)))
if left_node == right_node && left_segments == right_segments =>
{
Some((left_node.to_owned(), left_segments.to_vec()))
}
_ => None,
}
}
fn same_single_slot_dest(left: &PatchOp, right: &PatchOp) -> Option<SlotRef> {
match (single_slot_target(left), single_slot_target(right)) {
(Some(left), Some(right)) if left == right => Some(left.clone()),
_ => None,
}
}
fn same_ranked_dest(left: &PatchOp, right: &PatchOp) -> Option<RankedDest> {
match (ranked_target(left), ranked_target(right)) {
(Some(left), Some(right)) if left == right => Some(left.clone()),
_ => None,
}
}
fn node_target(op: &PatchOp) -> Option<&str> {
match op {
PatchOp::Replace { target_id, .. }
| PatchOp::Delete { target_id }
| PatchOp::Move { target_id, .. } => Some(target_id),
_ => None,
}
}
fn path_target(op: &PatchOp) -> Option<(&str, &[String])> {
match op {
PatchOp::Set { path, .. } | PatchOp::Clear { path } => {
Some((&path.node_id, &path.segments))
}
_ => None,
}
}
fn single_slot_target(op: &PatchOp) -> Option<&SlotRef> {
match op {
PatchOp::Put { slot, .. } => Some(slot),
PatchOp::Move {
dest: PatchDest::Slot(slot),
..
} => Some(slot),
_ => None,
}
}
fn ranked_target(op: &PatchOp) -> Option<&RankedDest> {
match op {
PatchOp::Insert { dest, .. } => Some(dest),
PatchOp::Move {
dest: PatchDest::Ranked(dest),
..
} => Some(dest),
_ => None,
}
}
fn first_stage(index: usize) -> ReplayStage {
ReplayStage::LeftOp(index)
}
fn second_stage(index: usize) -> ReplayStage {
ReplayStage::RightOp(index)
}
fn format_validation_errors(errors: &[draxl_validate::ValidationError]) -> String {
let mut message = String::from("validation failed:");
for error in errors {
message.push_str("\n- ");
message.push_str(&error.message);
}
message
}