307 lines
15 KiB
Rust
307 lines
15 KiB
Rust
//! Rust 测试框架验证语法根、生成 Visitor、原文定位和版本化 JSON 接口,不执行编译后代码。
|
|
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]
|
|
/// 由 Rust 测试框架调用:块内允许多个调用表达式语句及无 return 空体,但显式 return 必须位于块末尾。
|
|
fn function_bodies_accept_method_statements_and_optional_trailing_return() {
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
let root = parse_ast(
|
|
"function click(): Unit { title.setText(\"changed\") card.setColor(\"#FFEEDD\") }",
|
|
ParseMode::Module,
|
|
)
|
|
.unwrap();
|
|
// 模块模式必须产出 Program,不能把声明错误地包装成表达式或交互根。
|
|
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()
|
|
// 两个调用都应是普通 Expression 语句,不能被误识别为返回或绑定。
|
|
.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]
|
|
/// 由 Rust 测试框架调用:核对模块/表达式/单条交互的根类型与语句种类,文档范围有效,来源诊断保留 Unicode 行列。
|
|
fn parse_modes_produce_distinct_roots_and_preserve_source_diagnostics() {
|
|
// 保留与 AST 跨度相对应的原始 UTF-8 文本,后续不规范化空白或换行。
|
|
let source = "function echo(value: String): String { let copy = value return copy }";
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
let root = parse_ast(source, ParseMode::Module).unwrap();
|
|
// 模块模式必须产出 Program,不能把声明错误地包装成表达式或交互根。
|
|
let AstRoot::Module(program) = &root else {
|
|
panic!("expected module")
|
|
};
|
|
// 借用首个顶层函数,用于检查块内语句分类及顺序。
|
|
let function = program.functions().next().unwrap();
|
|
assert_eq!(function.body.statements.len(), 2);
|
|
// 第一条是 let Binding,不能与后面的 Return 混成同一语句种类。
|
|
assert!(matches!(
|
|
function.body.statements[0].kind,
|
|
StmtKind::Binding(_)
|
|
));
|
|
// 第二条明确为末尾 Return,顺序应与源码一致。
|
|
assert!(matches!(
|
|
function.body.statements[1].kind,
|
|
StmtKind::Return(_)
|
|
));
|
|
AstDocument::new(root)
|
|
.unwrap()
|
|
.validate_source(source)
|
|
.unwrap();
|
|
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
let root = parse_ast("echo(\"日\")", ParseMode::Expression).unwrap();
|
|
// Expression 模式只能返回表达式根,不带交互语句包装。
|
|
assert!(matches!(root, AstRoot::Expression(_)));
|
|
// 一条 let、一次调用和一个函数声明分别构成交互提交,均应只含一个语句。
|
|
for text in ["let value = \"日\"", "echo(value)", source] {
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
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());
|
|
// 给语法错误附会话来源身份,字符串中的日字使字节偏移与 Unicode 列不同。
|
|
let source = EndorReader::from_text("<session:1>", "\"日\" + )");
|
|
// 故意错误的快照解析必须返回带来源诊断,之后核对实际显示行列。
|
|
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("<session:1>:1:7:"));
|
|
}
|
|
|
|
#[test]
|
|
/// 由 Rust 测试框架调用:自定义访问器收集函数名与引用名,并验证子遍历提前失败后,成功进入的祖先节点退出钩子仍全部配平。
|
|
fn generated_visitors_reach_names_and_unwind_node_hooks_on_error() {
|
|
#[derive(Default)]
|
|
/// 本测试的可变访问器状态;节点只借用,收集文本拥有副本,depth 跟踪默认节点钩子的配平。
|
|
struct Names {
|
|
// 自定义 visit_function 记录声明名,和表达式引用名分开。
|
|
functions: Vec<String>,
|
|
// visit_expr 仅收集 ExprKind::Name,不包含类型注解或属性名。
|
|
references: Vec<String>,
|
|
// enter/leave 计数,遍历结束或提前失败后都应回到零。
|
|
depth: usize,
|
|
// 为 true 时在第一个引用名处主动失败,用来检查错误回溯退出路径。
|
|
stop: bool,
|
|
}
|
|
impl Visitor for Names {
|
|
type Error = &'static str;
|
|
/// 生成的默认 visit 方法成功进入节点时调用,记录当前已进入祖先数量。
|
|
fn enter_node(&mut self, _span: Span) -> Result<(), Self::Error> {
|
|
self.depth += 1;
|
|
Ok(())
|
|
}
|
|
/// 默认 visit 在子遍历完成或失败后调用,撤销此前成功进入的节点计数。
|
|
fn leave_node(&mut self) {
|
|
self.depth -= 1;
|
|
}
|
|
/// 自定义函数访问入口先记录名称,再显式 walk_function 保持所有字段的递归访问。
|
|
fn visit_function(&mut self, node: &Function) -> Result<(), Self::Error> {
|
|
self.functions.push(node.name.text.clone());
|
|
walk_function(self, node)
|
|
}
|
|
/// 自定义表达式访问入口只特殊处理引用名;其余结构继续由 walk_expr 分派。
|
|
fn visit_expr(&mut self, node: &Expr) -> Result<(), Self::Error> {
|
|
// 仅变量引用表达式进入收集/主动失败分支,字面量等节点仍照常递归。
|
|
if let ExprKind::Name(name) = &node.kind {
|
|
// 在调用 walk_expr 之前失败;默认祖先 visit 必须仍执行各自的 leave_node。
|
|
if self.stop {
|
|
return Err("stopped at reference");
|
|
}
|
|
self.references.push(name.clone());
|
|
}
|
|
walk_expr(self, node)
|
|
}
|
|
}
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
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]
|
|
/// 由 Rust 测试框架调用:括号范围对应原文,CRLF 按一行、Unicode 按一列;安全定位拒绝逆序、越界及切断字符的范围。
|
|
fn spans_share_unicode_locations_and_safe_original_source_access() {
|
|
// 保留与 AST 跨度相对应的原始 UTF-8 文本,后续不规范化空白或换行。
|
|
let source = " \r\n(値)";
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
let root = parse_ast(source, ParseMode::Expression).unwrap();
|
|
assert_eq!(root.source_text(source), Some("(値)"));
|
|
// 用同一原文把根节点的字节范围转换成一基行列,CRLF 只增加一行。
|
|
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]
|
|
/// 由 Rust 测试框架调用:三个语法根 JSON 往返不丢文本/数值拼写,文档 schema 与生成的 JSON Schema/TypeScript 标识一致。
|
|
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),
|
|
] {
|
|
// 用当前 schema 包装合法解析树并验证结构,为后续往返或定点破坏提供基线。
|
|
let document = AstDocument::new(parse_ast(source, mode).unwrap()).unwrap();
|
|
// 序列化前接口再次验证文档;输出应保留 schema、节点种类和原文字节范围。
|
|
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));
|
|
// 模块案例包含带前导零和尾零的长数值,JSON 必须保留原拼写而非转浮点数。
|
|
if mode == ParseMode::Module {
|
|
assert!(json.contains("00012345678901234567890.00100"));
|
|
}
|
|
}
|
|
// 读取仓库已生成的 JSON Schema,只检查标识与常量,不在测试中重新生成文件。
|
|
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]
|
|
/// 由 Rust 测试框架调用:分别拒绝错误 schema、未知字段、子节点越界、逆序范围以及对原文切断 UTF-8 字符的范围。
|
|
fn json_interfaces_reject_unknown_fields_schema_changes_and_invalid_ranges() {
|
|
// 保留与 AST 跨度相对应的原始 UTF-8 文本,后续不规范化空白或换行。
|
|
let source = "(f(値))";
|
|
// 用当前 schema 包装合法解析树并验证结构,为后续往返或定点破坏提供基线。
|
|
let document = AstDocument::new(parse_ast(source, ParseMode::Expression).unwrap()).unwrap();
|
|
// 用可编辑 JSON 值破坏一个属性;每个错误案例之后重新从合法 document 创建。
|
|
let mut value = serde_json::to_value(&document).unwrap();
|
|
// 首先只破坏版本身份,应在 schema 比较阶段报告 SchemaMismatch。
|
|
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();
|
|
// 恢复合法 JSON 后增加未知字段,生成类型的 deny_unknown_fields 应在解码阶段拒绝。
|
|
value["root"]["data"]["unexpected"] = true.into();
|
|
assert!(matches!(
|
|
AstDocument::from_json(&value.to_string()),
|
|
Err(AstError::Json(_))
|
|
));
|
|
value = serde_json::to_value(&document).unwrap();
|
|
// 再次恢复后缩小父范围,子节点仍在原位置,应报告 ChildOutsideParent。
|
|
value["root"]["data"]["span"]["end"] = 1.into();
|
|
assert!(matches!(
|
|
AstDocument::from_json(&value.to_string()),
|
|
Err(AstError::ChildOutsideParent { .. })
|
|
));
|
|
// 取出合法表达式后直接破坏其范围,验证 new 也拒绝无效手工构造树。
|
|
let AstRoot::Expression(mut expression) = document.root else {
|
|
unreachable!()
|
|
};
|
|
// 无需原文也能识别 end 小于 start 的逆序范围。
|
|
expression.span = Span::new(5, 4);
|
|
assert!(matches!(
|
|
AstDocument::new(AstRoot::Expression(expression)),
|
|
Err(AstError::InvalidSpan(_))
|
|
));
|
|
// 无原文时范围本身合法,但放回三字节字符原文时起点切断 UTF-8,应由 validate_source 拒绝。
|
|
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]
|
|
/// 由 Rust 测试框架调用:普通赋值使用独立 AST 节点并保留 Unicode 原文;链式赋值和非成员调用目标赋值语法失败。
|
|
fn bare_assignment_has_its_own_ast_and_unicode_source_ranges() {
|
|
// 保留与 AST 跨度相对应的原始 UTF-8 文本,后续不规范化空白或换行。
|
|
let source = "标题 = ui.text(\"你好\")";
|
|
// 仅解析指定模式的源码,取得拥有型 AstRoot,不分配 VM 对象。
|
|
let root = parse_ast(source, ParseMode::Interactive).unwrap();
|
|
// 解构交互根并借用其单条语句,避免只检查解析成功而忽略根形状。
|
|
let AstRoot::Interactive(input) = &root else {
|
|
panic!("interactive input")
|
|
};
|
|
// 普通赋值不是 let Binding,也不是成员赋值;名称节点和整句范围都要保持原文。
|
|
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));
|
|
// 用当前 schema 包装合法解析树并验证结构,为后续往返或定点破坏提供基线。
|
|
let document = AstDocument::new(root).unwrap();
|
|
document.validate_source(source).unwrap();
|
|
// 序列化前接口再次验证文档;输出应保留 schema、节点种类和原文字节范围。
|
|
let json = document.to_json().unwrap();
|
|
assert_eq!(AstDocument::from_json(&json).unwrap(), document);
|
|
assert!(parse_ast("value = 1 = 2", ParseMode::Interactive).is_err());
|
|
// 单独解析属性写入,确保普通赋值的新增路径没有抢走 MemberAssignment。
|
|
let member = parse_ast("object.field = 1", ParseMode::Interactive).unwrap();
|
|
// 必须检查节点类别,不能只根据字符串包含点就假定走了成员赋值规则。
|
|
let AstRoot::Interactive(member) = member else {
|
|
panic!("interactive")
|
|
};
|
|
// 属性写入使用 MemberAssignment,不能退化为普通目标名赋值。
|
|
assert!(matches!(
|
|
&member.statements[0].kind,
|
|
StmtKind::MemberAssignment(_)
|
|
));
|
|
assert!(parse_ast("object() = 1", ParseMode::Interactive).is_err());
|
|
}
|