use frontend::ast::visitor::{Visitor, walk_expr, walk_function}; use frontend::ast::{ AST_SCHEMA_ID, AstDocument, AstError, AstRoot, Expr, ExprKind, Function, Spanned, StmtKind, }; use frontend::{EndorReader, ParseMode, Span, parse_ast, parse_source_ast}; #[test] fn function_bodies_accept_method_statements_and_optional_trailing_return() { let root = parse_ast( "function click(): Unit { title.setText(\"changed\") card.setColor(\"#FFEEDD\") }", ParseMode::Module, ) .unwrap(); let AstRoot::Module(program) = root else { panic!("expected module") }; let function = program.functions().next().unwrap(); assert_eq!(function.body.statements.len(), 2); assert!( function .body .statements .iter() .all(|s| matches!(s.kind, StmtKind::Expression(_))) ); assert!(parse_ast("function empty(): Unit {}", ParseMode::Module).is_ok()); assert!( parse_ast( "function invalid(): Unit { return value other() }", ParseMode::Module ) .is_err() ); } #[test] fn parse_modes_produce_distinct_roots_and_preserve_source_diagnostics() { let source = "function echo(value: String): String { let copy = value return copy }"; let root = parse_ast(source, ParseMode::Module).unwrap(); let AstRoot::Module(program) = &root else { panic!("expected module") }; let function = program.functions().next().unwrap(); assert_eq!(function.body.statements.len(), 2); assert!(matches!( function.body.statements[0].kind, StmtKind::Binding(_) )); assert!(matches!( function.body.statements[1].kind, StmtKind::Return(_) )); AstDocument::new(root) .unwrap() .validate_source(source) .unwrap(); let root = parse_ast("echo(\"日\")", ParseMode::Expression).unwrap(); assert!(matches!(root, AstRoot::Expression(_))); for text in ["let value = \"日\"", "echo(value)", source] { let root = parse_ast(text, ParseMode::Interactive).unwrap(); let AstRoot::Interactive(input) = &root else { panic!("expected interactive") }; assert_eq!(input.statements.len(), 1); AstDocument::new(root) .unwrap() .validate_source(text) .unwrap(); } assert!(parse_ast("let x = 1 let y = 2", ParseMode::Interactive).is_err()); assert!(parse_ast("", ParseMode::Interactive).is_err()); let source = EndorReader::from_text("", "\"日\" + )"); let error = parse_source_ast(&source, ParseMode::Expression).unwrap_err(); assert_eq!(error.location.line, 1); assert_eq!(error.location.column, 7); assert!(error.to_string().starts_with(":1:7:")); } #[test] fn generated_visitors_reach_names_and_unwind_node_hooks_on_error() { #[derive(Default)] struct Names { functions: Vec, references: Vec, depth: usize, stop: bool, } impl Visitor for Names { type Error = &'static str; fn enter_node(&mut self, _span: Span) -> Result<(), Self::Error> { self.depth += 1; Ok(()) } fn leave_node(&mut self) { self.depth -= 1; } fn visit_function(&mut self, node: &Function) -> Result<(), Self::Error> { self.functions.push(node.name.text.clone()); walk_function(self, node) } fn visit_expr(&mut self, node: &Expr) -> Result<(), Self::Error> { if let ExprKind::Name(name) = &node.kind { if self.stop { return Err("stopped at reference"); } self.references.push(name.clone()); } walk_expr(self, node) } } let root = parse_ast( "function echo(value: String): String { let copy = value return copy }", ParseMode::Module, ) .unwrap(); let mut names = Names::default(); names.visit_ast_root(&root).unwrap(); assert_eq!(names.functions, ["echo"]); assert_eq!(names.references, ["value", "copy"]); assert_eq!(names.depth, 0); let mut stopped = Names { stop: true, ..Names::default() }; assert_eq!(stopped.visit_ast_root(&root), Err("stopped at reference")); assert_eq!(stopped.depth, 0); } #[test] fn spans_share_unicode_locations_and_safe_original_source_access() { let source = " \r\n(値)"; let root = parse_ast(source, ParseMode::Expression).unwrap(); assert_eq!(root.source_text(source), Some("(値)")); let location = root.location(source).unwrap(); assert_eq!((location.line, location.column), (2, 1)); assert_eq!(Span::new(1, 3).slice("値"), None); assert_eq!(Span::new(2, 1).slice("値"), None); assert_eq!(Span::new(0, 4).location("値"), None); } #[test] fn versioned_json_roundtrips_all_roots_and_preserves_number_spelling() { for (source, mode) in [ ( "function f(): Number { return 00012345678901234567890.00100 }", ParseMode::Module, ), ("f(.名前, \"日\\n\")", ParseMode::Expression), ("let value = 1", ParseMode::Interactive), ] { let document = AstDocument::new(parse_ast(source, mode).unwrap()).unwrap(); let json = document.to_json().unwrap(); let restored = AstDocument::from_json(&json).unwrap(); assert_eq!(restored, document); restored.validate_source(source).unwrap(); assert!(json.contains(AST_SCHEMA_ID)); if mode == ParseMode::Module { assert!(json.contains("00012345678901234567890.00100")); } } let schema: serde_json::Value = serde_json::from_str(include_str!("../editor/ast.schema.json")).unwrap(); assert_eq!(schema["$id"], AST_SCHEMA_ID); assert_eq!( schema["$defs"]["AstDocument"]["properties"]["schema"]["const"], AST_SCHEMA_ID ); assert!(include_str!("../editor/ast.ts").contains(AST_SCHEMA_ID)); } #[test] fn json_interfaces_reject_unknown_fields_schema_changes_and_invalid_ranges() { let source = "(f(値))"; let document = AstDocument::new(parse_ast(source, ParseMode::Expression).unwrap()).unwrap(); let mut value = serde_json::to_value(&document).unwrap(); value["schema"] = "urn:endor:ast:unknown".into(); assert!(matches!( AstDocument::from_json(&value.to_string()), Err(AstError::SchemaMismatch { .. }) )); value = serde_json::to_value(&document).unwrap(); value["root"]["data"]["unexpected"] = true.into(); assert!(matches!( AstDocument::from_json(&value.to_string()), Err(AstError::Json(_)) )); value = serde_json::to_value(&document).unwrap(); value["root"]["data"]["span"]["end"] = 1.into(); assert!(matches!( AstDocument::from_json(&value.to_string()), Err(AstError::ChildOutsideParent { .. }) )); let AstRoot::Expression(mut expression) = document.root else { unreachable!() }; expression.span = Span::new(5, 4); assert!(matches!( AstDocument::new(AstRoot::Expression(expression)), Err(AstError::InvalidSpan(_)) )); let root = AstRoot::Expression(Expr { kind: ExprKind::Name("値".into()), span: Span::new(1, 3), }); assert!(matches!( AstDocument::new(root).unwrap().validate_source("値"), Err(AstError::InvalidSpan(_)) )); } #[test] fn bare_assignment_has_its_own_ast_and_unicode_source_ranges() { let source = "标题 = ui.text(\"你好\")"; let root = parse_ast(source, ParseMode::Interactive).unwrap(); let AstRoot::Interactive(input) = &root else { panic!("interactive input") }; let StmtKind::Assignment(assignment) = &input.statements[0].kind else { panic!("assignment") }; assert_eq!(assignment.name.text, "标题"); assert_eq!(assignment.name.source_text(source), Some("标题")); assert_eq!(assignment.source_text(source), Some(source)); let document = AstDocument::new(root).unwrap(); document.validate_source(source).unwrap(); let json = document.to_json().unwrap(); assert_eq!(AstDocument::from_json(&json).unwrap(), document); assert!(parse_ast("value = 1 = 2", ParseMode::Interactive).is_err()); let member = parse_ast("object.field = 1", ParseMode::Interactive).unwrap(); let AstRoot::Interactive(member) = member else { panic!("interactive") }; assert!(matches!( &member.statements[0].kind, StmtKind::MemberAssignment(_) )); assert!(parse_ast("object() = 1", ParseMode::Interactive).is_err()); }