use proptest::prelude::*;
use ronin_core::{
apply_edit, parse, print, CstDocument, EditOperation, EditTarget, SyntaxKind, SyntaxNode,
TriviaPolicy,
};
fn all_editable_nodes(doc: &CstDocument) -> Vec<SyntaxNode> {
fn walk(n: SyntaxNode, out: &mut Vec<SyntaxNode>) {
for c in n.children() {
out.push(c.clone());
walk(c, out);
}
}
let mut out = Vec::new();
walk(doc.root(), &mut out);
out
}
fn policies() -> [TriviaPolicy; 4] {
[
TriviaPolicy::KEEP_ALL,
TriviaPolicy::DISCARD_ALL,
TriviaPolicy {
keep_leading: true,
keep_trailing: false,
},
TriviaPolicy {
keep_leading: false,
keep_trailing: true,
},
]
}
fn assert_keep_all_locality(src: &str, target: &SyntaxNode, edited: &CstDocument, kind_desc: &str) {
let r = target.text_range();
let prefix = &src[..r.start()];
let suffix = &src[r.end()..];
let out = print(edited);
assert!(
out.starts_with(prefix),
"{kind_desc}: prefix not preserved\n prefix={prefix:?}\n out={out:?}"
);
assert!(
out.ends_with(suffix),
"{kind_desc}: suffix not preserved\n suffix={suffix:?}\n out={out:?}"
);
}
#[test]
fn replace_keep_all_preserves_surrounding_bytes() {
let src = "Outer(a: Inner(x: 1), b: [10, 20, 30], c: { k: 'v' })";
let doc = parse(src);
for target in all_editable_nodes(&doc) {
if target.parent().map(|p| p.kind()) == Some(SyntaxKind::Root) {
continue;
}
let edited = apply_edit(
&doc,
EditOperation::replace(
EditTarget::Node(target.clone()),
"REPLACED",
TriviaPolicy::KEEP_ALL,
),
)
.expect("replace applies");
assert_keep_all_locality(src, &target, &edited, "replace");
assert!(print(&edited).contains("REPLACED"));
let _ = print(&edited);
assert_eq!(print(&doc), src, "original untouched");
}
}
#[test]
fn remove_keep_all_preserves_surrounding_bytes() {
let src = "Outer(a: Inner(x: 1), b: [10, 20, 30])";
let doc = parse(src);
for target in all_editable_nodes(&doc) {
if target.parent().map(|p| p.kind()) == Some(SyntaxKind::Root) {
continue;
}
let edited = apply_edit(
&doc,
EditOperation::remove(EditTarget::Node(target.clone()), TriviaPolicy::KEEP_ALL),
)
.expect("remove applies");
assert_keep_all_locality(src, &target, &edited, "remove");
assert_eq!(print(&doc), src, "original untouched");
}
}
#[test]
fn insert_keep_all_preserves_surrounding_bytes() {
let src = "[1, 2, 3]";
let doc = parse(src);
for target in all_editable_nodes(&doc) {
if target.parent().map(|p| p.kind()) == Some(SyntaxKind::Root) {
continue;
}
let r = target.text_range();
let prefix = &src[..r.start()];
let edited = apply_edit(
&doc,
EditOperation::insert(
EditTarget::Node(target.clone()),
"X",
TriviaPolicy::KEEP_ALL,
),
)
.expect("insert applies");
let out = print(&edited);
assert!(
out.starts_with(prefix),
"insert prefix not preserved: {out:?}"
);
assert!(out.contains('X'));
assert_eq!(print(&doc), src, "original untouched");
}
}
#[test]
fn discard_leading_trivia_drops_preceding_whitespace() {
let src = "Foo(a: 1, b: 2)";
let doc = parse(src);
let fields: Vec<_> = doc
.root()
.children()
.find(|n| n.kind() == SyntaxKind::Struct)
.unwrap()
.children()
.filter(|n| n.kind() == SyntaxKind::StructField)
.collect();
let b = fields[1].clone();
let edited = apply_edit(
&doc,
EditOperation::remove(
EditTarget::Node(b),
TriviaPolicy {
keep_leading: true, keep_trailing: true,
},
),
)
.unwrap();
assert_eq!(print(&edited), "Foo(a: 1, )");
let field = doc
.root()
.children()
.find(|n| n.kind() == SyntaxKind::Struct)
.unwrap()
.children()
.filter(|n| n.kind() == SyntaxKind::StructField)
.nth(1)
.unwrap();
let val = field
.children()
.find(|n| n.kind() == SyntaxKind::Literal)
.unwrap();
let edited2 = apply_edit(
&doc,
EditOperation::remove(
EditTarget::Node(val),
TriviaPolicy {
keep_leading: false,
keep_trailing: true,
},
),
)
.unwrap();
assert_eq!(print(&edited2), "Foo(a: 1, b:)");
}
proptest! {
#[test]
fn edits_are_local_and_printable(src in ron_strategy()) {
let doc = parse(&src);
let nodes = all_editable_nodes(&doc);
for target in nodes {
if target.parent().map(|p| p.kind()) == Some(SyntaxKind::Root) {
continue;
}
for policy in policies() {
let e = apply_edit(
&doc,
EditOperation::replace(EditTarget::Node(target.clone()), "0", policy),
).expect("replace applies");
let _ = print(&e); if policy == TriviaPolicy::KEEP_ALL {
assert_keep_all_locality(&src, &target, &e, "prop-replace");
}
let e = apply_edit(
&doc,
EditOperation::remove(EditTarget::Node(target.clone()), policy),
).expect("remove applies");
let _ = print(&e);
if policy == TriviaPolicy::KEEP_ALL {
assert_keep_all_locality(&src, &target, &e, "prop-remove");
}
let e = apply_edit(
&doc,
EditOperation::insert(EditTarget::Node(target.clone()), "9", policy),
).expect("insert applies");
let _ = print(&e);
}
prop_assert_eq!(print(&doc), src.clone());
}
}
}
fn ron_strategy() -> impl Strategy<Value = String> {
let leaf = prop_oneof![
any::<i32>().prop_map(|n| n.to_string()),
any::<bool>().prop_map(|b| b.to_string()),
Just("()".to_string()),
"[a-z]{1,4}".prop_map(|s| format!("\"{s}\"")),
"[a-z]".prop_map(|c| format!("'{c}'")),
];
leaf.prop_recursive(3, 32, 4, |inner| {
let kvs = prop::collection::vec(("[a-z]{1,4}", inner.clone()), 1..4);
prop_oneof![
prop::collection::vec(inner.clone(), 1..4).prop_map(|v| format!("[{}]", v.join(", "))),
prop::collection::vec(inner.clone(), 1..4).prop_map(|v| format!("({})", v.join(", "))),
kvs.clone().prop_map(|kvs| {
let body = kvs
.into_iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join(", ");
format!("{{{body}}}")
}),
("[A-Z][a-z]{0,4}", kvs).prop_map(|(name, kvs)| {
let body = kvs
.into_iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join(", ");
format!("{name}({body})")
}),
]
})
}