feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,640 @@
|
||||
use std::fs;
|
||||
use std::path::{Path as FilePath, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use dever_core::source::{SourceMap, Span};
|
||||
use dever_core::syntax::*;
|
||||
|
||||
#[path = "../../../crates/dever-cli/src/format.rs"]
|
||||
mod cli_format;
|
||||
|
||||
fn parsed_at(path: &FilePath, text: &str) -> Package {
|
||||
let mut sources = SourceMap::default();
|
||||
let id = sources.add(path, text);
|
||||
dever_core::parse(sources.get(id)).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn formatted_at(path: &FilePath, text: &str) -> String {
|
||||
let mut sources = SourceMap::default();
|
||||
let id = sources.add(path, text);
|
||||
dever_core::format::format(sources.get(id)).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_roundtrip(source: &str) -> String {
|
||||
assert_roundtrip_at(FilePath::new("main.dever"), source)
|
||||
}
|
||||
|
||||
fn assert_roundtrip_at(path: &FilePath, source: &str) -> String {
|
||||
let output = formatted_at(path, source);
|
||||
assert_eq!(
|
||||
shape(parsed_at(path, source)),
|
||||
shape(parsed_at(path, &output)),
|
||||
"{output}"
|
||||
);
|
||||
assert_eq!(
|
||||
formatted_at(path, &output),
|
||||
output,
|
||||
"formatter must be idempotent"
|
||||
);
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_layout_preserves_field_evaluation_order_and_groups() {
|
||||
let source = "type Pair{a:Int\nb:Int}\npublic main( )(result:Pair){result=Pair{b=second()\na=first()}}\n";
|
||||
assert_eq!(
|
||||
assert_roundtrip(source),
|
||||
"type Pair {\n a: Int\n b: Int\n}\n\npublic main() (result: Pair) {\n result = Pair {\n b = second()\n a = first()\n }\n}\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_syntax_and_literal_forms_roundtrip_without_semantic_checking() {
|
||||
let source = r#"type Empty {}
|
||||
type Event {Ready(bytes: Bytes, code: Int)
|
||||
End()
|
||||
Failed(message: Text)}
|
||||
public run(route: handler(value: Int) (answer: Int), stream: Stream<Event>, limit: Int >= -2 and < 8) (result: List<MapEntry<Text, Int?>>) {
|
||||
values = [1, 2, -3, 1.20, 3e-2, 5.0]
|
||||
mapping = {"z" = 1
|
||||
"a" = 2}
|
||||
result = []
|
||||
empty = {}
|
||||
truth = not false and true or false
|
||||
maths = -(-2) + 3 * (4 - 5) / 6 // 7 % 8
|
||||
compare = (1 < 2) == (3 <= 4) != (5 > 6) == (7 >= 8)
|
||||
literal = "引号\" 斜杠\\ 换行\n回车\r制表\t零\u{0}控制\u{1f}字符\u{4e2d}"
|
||||
maybe = null
|
||||
record = app.Pair {b = route(2)
|
||||
a = route(1)}
|
||||
record.a = route(3)
|
||||
call = (route(1)).answer
|
||||
each(consume, stream)
|
||||
}
|
||||
pick(event: Event.Ready(bytes, code)) (value: Int) {value=code}
|
||||
pick(event: other) (value: Int) {value=-1}
|
||||
constant(value: -2) (result: Int) {result=2}
|
||||
empty_generic(value: List<>) () {}
|
||||
"#;
|
||||
let output = assert_roundtrip(source);
|
||||
assert!(output.contains("\\u{0}"));
|
||||
assert!(output.contains("b = route(2)\n a = route(1)"));
|
||||
assert!(output.contains("End()"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_declarations_roundtrip_with_full_structure() {
|
||||
for (path, source) in [
|
||||
("system/health/api.dever", "cmd ping=app.ping\n"),
|
||||
("news/article/api.dever", "rest model\n"),
|
||||
(
|
||||
"notification/mail/adapter/worker.dever",
|
||||
"external exec \"worker\" {lib \"pip:fixture@1.0.0\"\nallow network}\n",
|
||||
),
|
||||
("user/session/job.dever", "schedule cleanup=\"0 * * * *\"\n"),
|
||||
] {
|
||||
assert_roundtrip_at(FilePath::new(path), source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_inside_expressions_signatures_and_eof_are_preserved() {
|
||||
let source = r#"# 模块说明
|
||||
# 类型说明
|
||||
type Empty { # 空类型
|
||||
# 保留空类型内部注释
|
||||
}
|
||||
public main( # 输入说明
|
||||
value: handler( # 回调输入
|
||||
value: Int # 数值
|
||||
) (answer: Int), # 回调输出
|
||||
values: List< # 元素类型
|
||||
Int
|
||||
>) (result: Int) { # 正文
|
||||
result=( # 分组
|
||||
1 # 第一项
|
||||
+ # 运算符
|
||||
2
|
||||
) # 结果
|
||||
list=[ # 列表
|
||||
1, # 成员
|
||||
# 下一成员
|
||||
2 # 最后一项
|
||||
]
|
||||
call=f(1 # 逗号之前
|
||||
,2)
|
||||
mapping={ # 映射
|
||||
"key"=2 # 映射值
|
||||
# 结束映射
|
||||
}
|
||||
# 结束正文
|
||||
} # 结束函数
|
||||
# 文件结尾
|
||||
# 无换行结尾"#;
|
||||
let output = assert_roundtrip(source);
|
||||
assert!(output.starts_with("# 模块说明\n# 类型说明\ntype Empty"));
|
||||
assert!(output.ends_with("# 文件结尾\n# 无换行结尾\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_positional_lists_wrap_with_two_space_indentation() {
|
||||
let source = "public main() () {send(\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\", \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\")}\n";
|
||||
let output = assert_roundtrip(source);
|
||||
assert!(output.contains(" send(\n \"aaaaaaaa"));
|
||||
assert!(output.contains("\",\n \"bbbbbbbb"));
|
||||
assert!(output.ends_with("\n )\n}\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_endings_comment_whitespace_and_unicode_are_canonical() {
|
||||
for newline in ["\n", "\r\n", "\r"] {
|
||||
let source = [
|
||||
"# 中文注释 ",
|
||||
"",
|
||||
"main() (value: Text) {value=\"# 😀\\u{2028}\\u{7f}\"} # 尾注释\t",
|
||||
"",
|
||||
]
|
||||
.join(newline);
|
||||
let output = assert_roundtrip(&source);
|
||||
assert!(!output.contains('\r'));
|
||||
assert!(output.starts_with("# 中文注释\n"));
|
||||
assert!(output.ends_with("} # 尾注释\n"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_examples_official_packages_and_complete_contract_roundtrip() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
for relative in ["library", "examples"] {
|
||||
let sources = SourceMap::load(&root.join(relative)).unwrap();
|
||||
for source in sources.files() {
|
||||
let path = source_root_relative(source.path());
|
||||
assert_roundtrip_at(&path, source.text());
|
||||
}
|
||||
}
|
||||
let prd = include_str!("../../../.trellis/tasks/09-04-dever-language-mvp/prd.md");
|
||||
let (_, example) = prd.split_once("## Complete Package Example\n").unwrap();
|
||||
let (_, fenced) = example.split_once("```dever\n").unwrap();
|
||||
assert_roundtrip(fenced.split_once("\n```").unwrap().0);
|
||||
}
|
||||
|
||||
fn source_root_relative(path: &FilePath) -> PathBuf {
|
||||
let segments = path.iter().collect::<Vec<_>>();
|
||||
let start = segments
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, segment)| {
|
||||
matches!(segment.to_str(), Some("module" | "test")).then_some(index + 1)
|
||||
})
|
||||
.next_back()
|
||||
.unwrap_or(0);
|
||||
segments[start..].iter().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_source_never_produces_formatted_output() {
|
||||
for text in ["package", "main() () {x =}", "# comment\n$"] {
|
||||
let mut sources = SourceMap::default();
|
||||
let id = sources.add("broken.dever", text);
|
||||
assert!(dever_core::format::format(sources.get(id)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_prepares_all_files_before_writes_and_check_is_read_only() {
|
||||
let temporary = Temporary::new();
|
||||
let module = temporary.0.join("module");
|
||||
fs::create_dir(&module).unwrap();
|
||||
let valid = "";
|
||||
fs::write(module.join("main.dever"), valid).unwrap();
|
||||
fs::write(module.join("broken.dever"), "package").unwrap();
|
||||
fs::write(module.join("unrelated.txt"), "unchanged").unwrap();
|
||||
let sources = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
assert!(cli_format::execute(&temporary.0, &sources, false).is_err());
|
||||
assert_eq!(
|
||||
fs::read_to_string(module.join("main.dever")).unwrap(),
|
||||
valid
|
||||
);
|
||||
fs::remove_file(module.join("broken.dever")).unwrap();
|
||||
let sources = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
assert_eq!(
|
||||
cli_format::execute(&temporary.0, &sources, true).unwrap(),
|
||||
ExitCode::FAILURE
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(module.join("main.dever")).unwrap(),
|
||||
valid
|
||||
);
|
||||
assert_eq!(
|
||||
cli_format::execute(&temporary.0, &sources, false).unwrap(),
|
||||
ExitCode::SUCCESS
|
||||
);
|
||||
let updated = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
assert_eq!(
|
||||
cli_format::execute(&temporary.0, &updated, true).unwrap(),
|
||||
ExitCode::SUCCESS
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(module.join("unrelated.txt")).unwrap(),
|
||||
"unchanged"
|
||||
);
|
||||
assert_eq!(fs::read_dir(&module).unwrap().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_path_set_changes_prevent_all_rewrites() {
|
||||
let temporary = Temporary::new();
|
||||
let module = temporary.0.join("module");
|
||||
fs::create_dir(&module).unwrap();
|
||||
let original = "";
|
||||
let existing = module.join("main.dever");
|
||||
let added = module.join("added.dever");
|
||||
fs::write(&existing, original).unwrap();
|
||||
let snapshot = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
fs::write(&added, "package").unwrap();
|
||||
assert!(cli_format::execute(&temporary.0, &snapshot, false).is_err());
|
||||
assert_eq!(fs::read_to_string(&existing).unwrap(), original);
|
||||
assert_eq!(fs::read_to_string(&added).unwrap(), "package");
|
||||
|
||||
fs::remove_file(&added).unwrap();
|
||||
fs::rename(&existing, &added).unwrap();
|
||||
assert!(cli_format::execute(&temporary.0, &snapshot, false).is_err());
|
||||
assert_eq!(fs::read_to_string(&added).unwrap(), original);
|
||||
assert!(!existing.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_snapshot_and_read_only_files_prevent_all_rewrites() {
|
||||
let temporary = Temporary::new();
|
||||
let module = temporary.0.join("module");
|
||||
fs::create_dir(&module).unwrap();
|
||||
let original = "";
|
||||
let first = module.join("first.dever");
|
||||
let second = module.join("second.dever");
|
||||
fs::write(&first, original).unwrap();
|
||||
fs::write(&second, original).unwrap();
|
||||
let sources = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
fs::write(&second, "edited").unwrap();
|
||||
assert!(cli_format::execute(&temporary.0, &sources, false).is_err());
|
||||
assert_eq!(fs::read_to_string(&first).unwrap(), original);
|
||||
fs::write(&second, original).unwrap();
|
||||
let permissions = fs::metadata(&second).unwrap().permissions();
|
||||
let mut read_only = permissions.clone();
|
||||
read_only.set_readonly(true);
|
||||
fs::set_permissions(&second, read_only).unwrap();
|
||||
let result = cli_format::execute(&temporary.0, &sources, false);
|
||||
fs::set_permissions(&second, permissions).unwrap();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(fs::read_to_string(&first).unwrap(), original);
|
||||
assert_eq!(fs::read_dir(&module).unwrap().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_and_test_sources_are_prepared_before_any_rewrite() {
|
||||
let temporary = Temporary::new();
|
||||
let module = temporary.0.join("module");
|
||||
let tests = temporary.0.join("test/user/account");
|
||||
fs::create_dir(&module).unwrap();
|
||||
fs::create_dir_all(&tests).unwrap();
|
||||
let main = module.join("main.dever");
|
||||
let test = tests.join("register.dever");
|
||||
let unformatted_main = "main()(){ }";
|
||||
let unformatted_test = "register()(){assert(true)}";
|
||||
fs::write(&main, unformatted_main).unwrap();
|
||||
fs::write(&test, unformatted_test).unwrap();
|
||||
let sources = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
|
||||
let permissions = fs::metadata(&test).unwrap().permissions();
|
||||
let mut read_only = permissions.clone();
|
||||
read_only.set_readonly(true);
|
||||
fs::set_permissions(&test, read_only).unwrap();
|
||||
let result = cli_format::execute(&temporary.0, &sources, false);
|
||||
fs::set_permissions(&test, permissions).unwrap();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(fs::read_to_string(&main).unwrap(), unformatted_main);
|
||||
assert_eq!(fs::read_to_string(&test).unwrap(), unformatted_test);
|
||||
|
||||
cli_format::execute(&temporary.0, &sources, false).unwrap();
|
||||
assert_eq!(fs::read_to_string(&main).unwrap(), "main() () {}\n");
|
||||
assert_eq!(
|
||||
fs::read_to_string(&test).unwrap(),
|
||||
"register() () {\n assert(true)\n}\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symbolic_links_are_rejected_and_permissions_are_preserved() {
|
||||
use std::os::unix::fs::{PermissionsExt, symlink};
|
||||
let temporary = Temporary::new();
|
||||
let module = temporary.0.join("module");
|
||||
fs::create_dir(&module).unwrap();
|
||||
let original = "";
|
||||
let file = module.join("main.dever");
|
||||
fs::write(&file, original).unwrap();
|
||||
symlink(&file, module.join("linked.dever")).unwrap();
|
||||
assert!(SourceMap::load_project(&module, &temporary.0.join("test")).is_err());
|
||||
assert_eq!(fs::read_to_string(&file).unwrap(), original);
|
||||
fs::remove_file(module.join("linked.dever")).unwrap();
|
||||
fs::set_permissions(&file, fs::Permissions::from_mode(0o640)).unwrap();
|
||||
let sources = SourceMap::load_project(&module, &temporary.0.join("test")).unwrap();
|
||||
cli_format::execute(&temporary.0, &sources, false).unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(&file).unwrap().permissions().mode() & 0o777,
|
||||
0o640
|
||||
);
|
||||
}
|
||||
|
||||
struct Temporary(PathBuf);
|
||||
|
||||
impl Temporary {
|
||||
fn new() -> Self {
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"dever-formatter-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Temporary {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Compare full syntax structure and decoded literals independently of source positions.
|
||||
fn shape(mut package: Package) -> Package {
|
||||
let span = Span {
|
||||
start: 0,
|
||||
end: 0,
|
||||
..package.span
|
||||
};
|
||||
package.span = span;
|
||||
normalize_path(&mut package.name, span);
|
||||
for comment in &mut package.comments {
|
||||
comment.span = span;
|
||||
comment.text = comment.text.trim_end().to_owned();
|
||||
}
|
||||
for declaration in &mut package.declarations {
|
||||
match declaration {
|
||||
Declaration::Api(binding) => {
|
||||
binding.span = span;
|
||||
binding.action.span = span;
|
||||
normalize_path(&mut binding.target, span);
|
||||
}
|
||||
Declaration::Rest(rest) => {
|
||||
rest.span = span;
|
||||
if let Some(model) = &mut rest.model {
|
||||
normalize_path(model, span);
|
||||
}
|
||||
}
|
||||
Declaration::External(external) => {
|
||||
external.span = span;
|
||||
external.entry_span = span;
|
||||
for name in external.libs.iter_mut().chain(&mut external.capabilities) {
|
||||
name.span = span;
|
||||
}
|
||||
}
|
||||
Declaration::Type(declaration) => {
|
||||
declaration.span = span;
|
||||
declaration.name.span = span;
|
||||
match &mut declaration.shape {
|
||||
TypeShape::Record(fields) => normalize_fields(fields, span),
|
||||
TypeShape::Choice(variants) => {
|
||||
for variant in variants {
|
||||
variant.span = span;
|
||||
variant.name.span = span;
|
||||
normalize_fields(&mut variant.payload, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Function(function) => {
|
||||
function.span = span;
|
||||
function.name.span = span;
|
||||
normalize_fields(&mut function.outputs, span);
|
||||
if let Some(failure) = &mut function.fails {
|
||||
normalize_type(failure, span);
|
||||
}
|
||||
for input in &mut function.inputs {
|
||||
input.span = span;
|
||||
input.name.span = span;
|
||||
match &mut input.kind {
|
||||
InputKind::Handler(signature) => {
|
||||
signature.span = span;
|
||||
normalize_fields(&mut signature.inputs, span);
|
||||
normalize_fields(&mut signature.outputs, span);
|
||||
}
|
||||
InputKind::Value(pattern) => match pattern {
|
||||
Pattern::Typed { ty, bounds } => {
|
||||
normalize_type(ty, span);
|
||||
for bound in bounds {
|
||||
bound.span = span;
|
||||
}
|
||||
}
|
||||
Pattern::Variant {
|
||||
name,
|
||||
arguments,
|
||||
bindings,
|
||||
span: pattern_span,
|
||||
} => {
|
||||
normalize_path(name, span);
|
||||
for argument in arguments {
|
||||
normalize_type(argument, span);
|
||||
}
|
||||
normalize_path(bindings, span);
|
||||
*pattern_span = span;
|
||||
}
|
||||
Pattern::Literal {
|
||||
span: pattern_span, ..
|
||||
}
|
||||
| Pattern::Other(pattern_span) => *pattern_span = span,
|
||||
},
|
||||
}
|
||||
}
|
||||
for statement in &mut function.body {
|
||||
statement.span = span;
|
||||
match &mut statement.kind {
|
||||
StatementKind::Assign { target, value } => {
|
||||
normalize_path(target, span);
|
||||
normalize_expression(value, span);
|
||||
}
|
||||
StatementKind::Call(call) => normalize_expression(call, span),
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Database(binding) => {
|
||||
binding.span = span;
|
||||
binding.name.span = span;
|
||||
}
|
||||
Declaration::Schedule(schedule) => {
|
||||
schedule.span = span;
|
||||
schedule.target.span = span;
|
||||
}
|
||||
Declaration::ModelIndex(index) => {
|
||||
index.span = span;
|
||||
normalize_path(&mut index.fields, span);
|
||||
}
|
||||
Declaration::Relation(relation) => {
|
||||
relation.span = span;
|
||||
relation.name.span = span;
|
||||
normalize_path(&mut relation.field, span);
|
||||
}
|
||||
Declaration::Seed(seed) => {
|
||||
seed.span = span;
|
||||
for row in &mut seed.rows {
|
||||
for field in row {
|
||||
field.span = span;
|
||||
field.name.span = span;
|
||||
normalize_expression(&mut field.value, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::Migration(migration) => {
|
||||
migration.span = span;
|
||||
migration.name.span = span;
|
||||
for operation in &mut migration.operations {
|
||||
match operation {
|
||||
MigrationOperation::Drop(field) => field.span = span,
|
||||
MigrationOperation::Sql(sql) => {
|
||||
sql.span = span;
|
||||
sql.sqlite.span = span;
|
||||
sql.postgres.span = span;
|
||||
for parameter in &mut sql.parameters {
|
||||
normalize_expression(parameter, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Declaration::ModelSql(sql) => {
|
||||
sql.span = span;
|
||||
sql.name.span = span;
|
||||
normalize_fields(&mut sql.inputs, span);
|
||||
normalize_fields(&mut sql.outputs, span);
|
||||
sql.sqlite.span = span;
|
||||
sql.postgres.span = span;
|
||||
}
|
||||
}
|
||||
}
|
||||
package
|
||||
}
|
||||
|
||||
fn normalize_path(path: &mut Path, span: Span) {
|
||||
for name in path {
|
||||
name.span = span;
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_fields(fields: &mut [Field], span: Span) {
|
||||
for field in fields {
|
||||
field.span = span;
|
||||
field.name.span = span;
|
||||
normalize_type(&mut field.ty, span);
|
||||
for bound in &mut field.bounds {
|
||||
bound.span = span;
|
||||
}
|
||||
if let Some(value) = &mut field.storage.default {
|
||||
normalize_expression(value, span);
|
||||
}
|
||||
if let Some(from) = &mut field.storage.from {
|
||||
from.span = span;
|
||||
}
|
||||
for expression in [
|
||||
&mut field.storage.owner,
|
||||
&mut field.storage.create,
|
||||
&mut field.storage.replace,
|
||||
&mut field.storage.search,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
normalize_expression(expression, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_type(ty: &mut TypeRef, span: Span) {
|
||||
ty.span = span;
|
||||
normalize_path(&mut ty.name, span);
|
||||
for argument in &mut ty.arguments {
|
||||
normalize_type(argument, span);
|
||||
}
|
||||
for parameter in &mut ty.parameters {
|
||||
parameter.span = span;
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_expression(expression: &mut Expression, span: Span) {
|
||||
expression.span = span;
|
||||
match &mut expression.kind {
|
||||
ExpressionKind::Literal(_) => {}
|
||||
ExpressionKind::Name(path) => normalize_path(path, span),
|
||||
ExpressionKind::Group(value)
|
||||
| ExpressionKind::Fail(value)
|
||||
| ExpressionKind::CaptureResult(value)
|
||||
| ExpressionKind::Unary { value, .. } => normalize_expression(value, span),
|
||||
ExpressionKind::Contextual(ContextualExpression::Channel { element, capacity }) => {
|
||||
normalize_type(element, span);
|
||||
normalize_expression(capacity, span);
|
||||
}
|
||||
ExpressionKind::Field { value, name } => {
|
||||
normalize_expression(value, span);
|
||||
name.span = span;
|
||||
}
|
||||
ExpressionKind::Call {
|
||||
function,
|
||||
arguments,
|
||||
} => {
|
||||
normalize_expression(function, span);
|
||||
for argument in arguments {
|
||||
normalize_expression(argument, span);
|
||||
}
|
||||
}
|
||||
ExpressionKind::Record { name, fields } => {
|
||||
normalize_path(name, span);
|
||||
for field in fields {
|
||||
field.span = span;
|
||||
field.name.span = span;
|
||||
normalize_expression(&mut field.value, span);
|
||||
}
|
||||
}
|
||||
ExpressionKind::List(values) => {
|
||||
for value in values {
|
||||
normalize_expression(value, span);
|
||||
}
|
||||
}
|
||||
ExpressionKind::Map(entries) => {
|
||||
for entry in entries {
|
||||
entry.span = span;
|
||||
normalize_expression(&mut entry.key, span);
|
||||
normalize_expression(&mut entry.value, span);
|
||||
}
|
||||
}
|
||||
ExpressionKind::Binary { left, right, .. } => {
|
||||
normalize_expression(left, span);
|
||||
normalize_expression(right, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user