rd-source 0.0.1

Parser for R documentation source files into rd-ast documents
Documentation
use rd_ast::{RdDocument, RdNode, RdNodeKind, RdPath, RdPathSegment};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionPresence {
    Absent,
    PresentEmpty,
    PresentNonEmpty,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiffKind {
    MissingNode,
    UnexpectedNode,
    NodeKindMismatch {
        expected: RdNodeKind,
        actual: RdNodeKind,
    },
    TagMismatch {
        expected: String,
        actual: String,
    },
    LeafTextMismatch {
        leaf_kind: RdNodeKind,
        expected: String,
        actual: String,
    },
    OptionPresenceMismatch {
        expected: OptionPresence,
        actual: OptionPresence,
    },
    ChildCountMismatch {
        expected: usize,
        actual: usize,
    },
    RawNodeNotComparable,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Difference {
    pub path: RdPath,
    pub kind: DiffKind,
}

fn escaped(s: &str) -> String {
    s.escape_debug().to_string()
}
fn kind(n: &RdNode) -> RdNodeKind {
    RdNodeKind::of(n)
}
fn option(n: &rd_ast::RdTagged) -> OptionPresence {
    match n.option() {
        None => OptionPresence::Absent,
        Some([]) => OptionPresence::PresentEmpty,
        Some(_) => OptionPresence::PresentNonEmpty,
    }
}
fn raw_option(n: &rd_ast::RawRdNode) -> OptionPresence {
    match n.option() {
        None => OptionPresence::Absent,
        Some([]) => OptionPresence::PresentEmpty,
        Some(_) => OptionPresence::PresentNonEmpty,
    }
}

pub fn compare(expected: &RdDocument, actual: &RdDocument) -> Vec<Difference> {
    let mut out = Vec::new();
    if expected.nodes().len() != actual.nodes().len() {
        out.push(Difference {
            path: RdPath::new(Vec::new()),
            kind: DiffKind::ChildCountMismatch {
                expected: expected.nodes().len(),
                actual: actual.nodes().len(),
            },
        });
    }
    let common = expected.nodes().len().min(actual.nodes().len());
    for i in 0..common {
        compare_node(
            &expected.nodes()[i],
            &actual.nodes()[i],
            RdPath::new(vec![RdPathSegment::TopLevel(i)]),
            &mut out,
        );
    }
    for i in common..expected.nodes().len() {
        out.push(Difference {
            path: RdPath::new(vec![RdPathSegment::TopLevel(i)]),
            kind: DiffKind::MissingNode,
        });
    }
    for i in common..actual.nodes().len() {
        out.push(Difference {
            path: RdPath::new(vec![RdPathSegment::TopLevel(i)]),
            kind: DiffKind::UnexpectedNode,
        });
    }
    out
}

fn compare_node(expected: &RdNode, actual: &RdNode, path: RdPath, out: &mut Vec<Difference>) {
    if kind(expected) != kind(actual) {
        out.push(Difference {
            path,
            kind: DiffKind::NodeKindMismatch {
                expected: kind(expected),
                actual: kind(actual),
            },
        });
        return;
    }
    match (expected, actual) {
        (RdNode::Text(a), RdNode::Text(b))
        | (RdNode::RCode(a), RdNode::RCode(b))
        | (RdNode::Verb(a), RdNode::Verb(b))
        | (RdNode::Comment(a), RdNode::Comment(b)) => {
            if a != b {
                out.push(Difference {
                    path,
                    kind: DiffKind::LeafTextMismatch {
                        leaf_kind: kind(expected),
                        expected: escaped(a),
                        actual: escaped(b),
                    },
                });
            }
        }
        (RdNode::Tagged(a), RdNode::Tagged(b)) => {
            if a.tag() != b.tag() {
                out.push(Difference {
                    path: path.clone(),
                    kind: DiffKind::TagMismatch {
                        expected: a.tag().as_rd_tag().to_string(),
                        actual: b.tag().as_rd_tag().to_string(),
                    },
                });
            }
            if option(a) != option(b) {
                out.push(Difference {
                    path: path.with_option(),
                    kind: DiffKind::OptionPresenceMismatch {
                        expected: option(a),
                        actual: option(b),
                    },
                });
            }
            compare_children(a.children(), b.children(), path.clone(), out);
            if let (Some(x), Some(y)) = (a.option(), b.option()) {
                compare_children(x, y, path.with_option(), out);
            }
        }
        (RdNode::Group(a), RdNode::Group(b)) => {
            compare_children(a.children(), b.children(), path, out)
        }
        (RdNode::Raw(a), RdNode::Raw(b)) => {
            if a.tag() != b.tag()
                || a.payload() != b.payload()
                || a.children() != b.children()
                || a.attributes() != b.attributes()
                || raw_option(a) != raw_option(b)
                || a.option() != b.option()
            {
                out.push(Difference {
                    path,
                    kind: DiffKind::RawNodeNotComparable,
                });
            }
        }
        _ => out.push(Difference {
            path,
            kind: DiffKind::RawNodeNotComparable,
        }),
    }
}

fn compare_children(
    expected: &[RdNode],
    actual: &[RdNode],
    path: RdPath,
    out: &mut Vec<Difference>,
) {
    if expected.len() != actual.len() {
        out.push(Difference {
            path: path.clone(),
            kind: DiffKind::ChildCountMismatch {
                expected: expected.len(),
                actual: actual.len(),
            },
        });
    }
    let common = expected.len().min(actual.len());
    for i in 0..common {
        compare_node(&expected[i], &actual[i], path.with_child(i), out);
    }
    for i in common..expected.len() {
        out.push(Difference {
            path: path.with_child(i),
            kind: DiffKind::MissingNode,
        });
    }
    for i in common..actual.len() {
        out.push(Difference {
            path: path.with_child(i),
            kind: DiffKind::UnexpectedNode,
        });
    }
}

impl std::fmt::Display for Difference {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}: {:?}", self.path, self.kind)
    }
}