use ronin_core::ast::{Document, Value};
use ronin_core::{
apply_edit, parse, parse_bytes, print, CstDocument, Diagnostic, DiagnosticCode, EditOperation,
EditTarget, Severity, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TriviaPolicy,
};
type PublicApiWitness<'a> = (
&'a CstDocument,
&'a SyntaxNode,
&'a SyntaxToken,
SyntaxKind,
TextRange,
&'a Diagnostic,
Severity,
DiagnosticCode,
);
#[test]
fn parse_navigate_diagnostics_print_round_trip() {
let src = "Config(\n name: \"demo\",\n count: 3, // trailing comment\n tags: [\"a\", \"b\"],\n)\n";
let doc: CstDocument = parse(src);
assert_eq!(
print(&doc),
src,
"consumer round-trip must be byte-identical"
);
assert_eq!(doc.source_len(), src.len());
let diags: &[Diagnostic] = doc.diagnostics();
assert!(diags.is_empty(), "well-formed input has no diagnostics");
let root: SyntaxNode = doc.root();
assert_eq!(root.kind(), SyntaxKind::Root);
let struct_node = root
.children()
.find(|n| n.kind() == SyntaxKind::Struct)
.expect("top-level struct");
let name_tok: SyntaxToken = struct_node
.first_token_of(SyntaxKind::Ident)
.expect("struct name token");
assert_eq!(name_tok.text(), "Config");
let _rng: TextRange = name_tok.text_range();
let document = Document::cast(doc.root()).expect("root casts to Document");
let Some(Value::Struct(s)) = document.value() else {
panic!("expected a struct value");
};
assert_eq!(s.name_text().as_deref(), Some("Config"));
let field_names: Vec<String> = s.fields().filter_map(|f| f.name_text()).collect();
assert_eq!(field_names, vec!["name", "count", "tags"]);
let tags = s
.fields()
.find(|f| f.name_text().as_deref() == Some("tags"))
.expect("tags field");
let Some(Value::List(list)) = tags.value() else {
panic!("tags should be a list");
};
let items: Vec<Value> = list.items().collect();
assert_eq!(items.len(), 2);
let code = DiagnosticCode::UnexpectedToken;
let dummy = Diagnostic::new(code, TextRange::new(0, 0), "x");
let witness: PublicApiWitness<'_> = (
&doc,
&struct_node,
&name_tok,
SyntaxKind::Struct,
TextRange::new(0, 1),
&dummy,
Severity::Error,
code,
);
assert_eq!(witness.0.source_len(), src.len());
assert_eq!(witness.3, SyntaxKind::Struct);
}
#[test]
fn diagnostics_are_reachable_for_malformed_input() {
let src = "[1, 2, 3";
let doc = parse(src);
assert_eq!(print(&doc), src, "error-recovered tree round-trips");
let diags = doc.diagnostics();
assert!(!diags.is_empty(), "malformed input yields diagnostics");
let d = &diags[0];
assert_eq!(d.code(), DiagnosticCode::UnclosedDelimiter);
assert_eq!(d.severity(), Severity::Error);
assert!(d.range().end() <= doc.source_len());
assert_eq!(d.code().code(), "RON-P0002");
}
#[test]
fn parse_bytes_and_utf8_rejection_via_public_api() {
let doc = parse_bytes("Some(1)".as_bytes()).expect("valid UTF-8");
assert_eq!(print(&doc), "Some(1)");
let err = parse_bytes(&[0xFF, 0x00]).unwrap_err();
assert!(!err.to_string().is_empty());
}
#[test]
fn edit_via_public_api_preserves_unaffected_regions() {
let src = "Foo(x: 1, y: 2)";
let doc = parse(src);
let strukt = doc
.root()
.children()
.find(|n| n.kind() == SyntaxKind::Struct)
.unwrap();
let name = strukt.first_token_of(SyntaxKind::Ident).unwrap();
let edit = EditOperation::replace(
EditTarget::TokenSpan {
first: name.clone(),
last: name,
},
"Bar",
TriviaPolicy::KEEP_ALL,
);
let edited = apply_edit(&doc, edit).expect("edit applies");
assert_eq!(print(&edited), "Bar(x: 1, y: 2)");
assert_eq!(print(&doc), src);
}