Preserve source comments and layout through model token edits
This commit is contained in:
@@ -686,3 +686,385 @@ fn include_inspection_returns_located_failure_without_partial_discovery_and_is_b
|
||||
);
|
||||
assert_eq!(errors[0].diagnostic.code, "project_limit");
|
||||
}
|
||||
|
||||
fn comments(source: &str) -> Vec<String> {
|
||||
language::tokenize(source)
|
||||
.into_iter()
|
||||
.filter(|token| token.class == language::TokenClass::Comment)
|
||||
.map(|token| source[token.span.start..token.span.end].to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_spelling() {
|
||||
let entry = "arcadia 3;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
||||
let source = "oa_state_machine machine \"Machine\" {}\r\noa_region region_one \"Region\" { parent @machine; }\r\noa_state first \"First\" { parent @region_one; }\r\noa_state second \"Second\" { parent @region_one; }\r\nstate_transition route // route title\r\n \"Original transition\" {\r\n\tguard // feasibility\r\n\t \"old guard\"; // retain explanation\r\n\tdescription \"Keep\\u0020spelling\";\r\n\tsource @ // endpoint proof\r\n first; target @second;\r\n}\r\n";
|
||||
let current = compile(&bundle(entry, &[("behavior.arcadia", source)]));
|
||||
let mut project = current.decoded.project.clone();
|
||||
project.name = "Renamed project".into();
|
||||
project.description = "After".into();
|
||||
let route = project
|
||||
.relations
|
||||
.get_mut(&id(¤t.decoded.project, "route"))
|
||||
.unwrap();
|
||||
route.name = "Renamed transition".into();
|
||||
if let RelationBody::StateTransition { guard, .. } = &mut route.body {
|
||||
*guard = "new guard".into();
|
||||
} else {
|
||||
panic!("transition");
|
||||
}
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
result.bundle.files["project.arcadia"],
|
||||
current.bundle.files["project.arcadia"]
|
||||
.replace("\"Original project\"", "\"Renamed project\"")
|
||||
.replace("\"Before\"", "\"After\"")
|
||||
);
|
||||
assert_eq!(
|
||||
result.bundle.files["behavior.arcadia"],
|
||||
current.bundle.files["behavior.arcadia"]
|
||||
.replace("\"Original transition\"", "\"Renamed transition\"")
|
||||
.replace("\"old guard\"", "\"new guard\"")
|
||||
);
|
||||
assert_eq!(result.decoded.project, project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after_at() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity root_work // root rationale\n \"Root\\u0020name\" { description \"root_work is prose\"; }\noa_activity child \"Child\" { parent @ // parent proof\n root_work; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
project
|
||||
.elements
|
||||
.get_mut(&id(¤t.decoded.project, "root_work"))
|
||||
.unwrap()
|
||||
.alias = "renamed_work".into();
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let expected = current.bundle.files["work.arcadia"]
|
||||
.replace("oa_activity root_work ", "oa_activity renamed_work ")
|
||||
.replace("\n root_work;", "\n renamed_work;");
|
||||
assert_eq!(result.bundle.files["work.arcadia"], expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
|
||||
&[(
|
||||
"work.arcadia",
|
||||
"oa_activity work \"Work\" {\r\n\t// owner rationale\r\n\tparent none ; // preserve spacing\r\n}\r\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
project
|
||||
.elements
|
||||
.get_mut(&id(¤t.decoded.project, "work"))
|
||||
.unwrap()
|
||||
.description = "New documentation".into();
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let before = ¤t.bundle.files["work.arcadia"];
|
||||
let after = &result.bundle.files["work.arcadia"];
|
||||
assert_eq!(
|
||||
after.replace("\tdescription \"New documentation\";\r\n", ""),
|
||||
*before
|
||||
);
|
||||
assert_eq!(comments(after), comments(before));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_change_removes_field_syntax_but_keeps_its_comments_and_other_fields() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"item.arcadia\"; }",
|
||||
&[(
|
||||
"item.arcadia",
|
||||
"oa_exchange_item item \"Item\" {\n description \"Keep\\u0020this\" ; // description note\n kind // classification rationale\n Information ;\n parent none;\n}\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
project
|
||||
.elements
|
||||
.get_mut(&id(¤t.decoded.project, "item"))
|
||||
.unwrap()
|
||||
.body = ElementBody::new(Kind::Entity, Layer::Oa).unwrap();
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let after = &result.bundle.files["item.arcadia"];
|
||||
assert!(after.contains("description \"Keep\\u0020this\" ; // description note"));
|
||||
assert!(after.contains("parent none;"));
|
||||
assert_eq!(
|
||||
comments(after),
|
||||
comments(¤t.bundle.files["item.arcadia"])
|
||||
);
|
||||
assert!(!significant_words(after).contains(&"Information".into()));
|
||||
}
|
||||
|
||||
fn significant_words(source: &str) -> Vec<String> {
|
||||
language::tokenize(source)
|
||||
.into_iter()
|
||||
.filter(|token| {
|
||||
!matches!(
|
||||
token.class,
|
||||
language::TokenClass::Whitespace | language::TokenClass::Comment
|
||||
)
|
||||
})
|
||||
.map(|token| source[token.span.start..token.span.end].to_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comments() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"event.arcadia\"; }",
|
||||
&[(
|
||||
"event.arcadia",
|
||||
"oa_event timer \"Timer\" { body { Time // time rationale\n { expression // expression rationale\n \"before\" ; kind After; // clock rationale\n }; }; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
let timer = project
|
||||
.elements
|
||||
.get_mut(&id(¤t.decoded.project, "timer"))
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Event { body }) = timer.shared_mut() {
|
||||
*body = EventBody::Change {
|
||||
expression: "after".into(),
|
||||
};
|
||||
} else {
|
||||
panic!("event");
|
||||
}
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let after = &result.bundle.files["event.arcadia"];
|
||||
assert!(after.contains("Change // time rationale"));
|
||||
assert!(after.contains("expression // expression rationale\n \"after\" ;"));
|
||||
assert_eq!(
|
||||
comments(after),
|
||||
comments(¤t.bundle.files["event.arcadia"])
|
||||
);
|
||||
assert!(!significant_words(after).contains(&"After".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"ports.arcadia\"; }",
|
||||
&[(
|
||||
"ports.arcadia",
|
||||
"sa_function work \"Work\" {}\nsa_port input \"Input\" { parent @work; contract Legacy; // contract rationale\n legacy { direction In; // direction rationale\n interface none; // interface rationale\n }; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
let port = project
|
||||
.elements
|
||||
.get_mut(&id(¤t.decoded.project, "input"))
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Port { contract, legacy }) = port.shared_mut() {
|
||||
*contract = PortContract::FunctionInput {
|
||||
incoming_items: vec![],
|
||||
};
|
||||
*legacy = None;
|
||||
} else {
|
||||
panic!("port");
|
||||
}
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
comments(&result.bundle.files["ports.arcadia"]),
|
||||
comments(¤t.bundle.files["ports.arcadia"])
|
||||
);
|
||||
assert_eq!(result.decoded.project, project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_identity() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
let a = id(&project, "a");
|
||||
let b = id(&project, "b");
|
||||
let service = id(&project, "service");
|
||||
project.elements.get_mut(&a).unwrap().alias = "renamed_a".into();
|
||||
if let Some(SharedBody::Interface { items }) =
|
||||
project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
{
|
||||
*items = vec![b, a];
|
||||
} else {
|
||||
panic!("interface");
|
||||
}
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let after = &result.bundle.files["types.arcadia"];
|
||||
assert!(after.find("// before B").unwrap() < after.find("// before A").unwrap());
|
||||
assert!(after.contains("@ // A reference rationale\n renamed_a // after A"));
|
||||
let mut old_comments = comments(¤t.bundle.files["types.arcadia"]);
|
||||
let mut new_comments = comments(after);
|
||||
old_comments.sort();
|
||||
new_comments.sort();
|
||||
assert_eq!(old_comments, new_comments);
|
||||
assert_eq!(result.decoded.project, project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }\n",
|
||||
)],
|
||||
);
|
||||
let current = compile(&input);
|
||||
let mut project = current.decoded.project.clone();
|
||||
let a = id(&project, "a");
|
||||
let b = id(&project, "b");
|
||||
let service = id(&project, "service");
|
||||
project.elements.remove(&a);
|
||||
if let Some(SharedBody::Interface { items }) =
|
||||
project.elements.get_mut(&service).unwrap().shared_mut()
|
||||
{
|
||||
*items = vec![b];
|
||||
} else {
|
||||
panic!("interface");
|
||||
}
|
||||
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
let mut before = comments(¤t.bundle.files["types.arcadia"]);
|
||||
let mut after = comments(&result.bundle.files["types.arcadia"]);
|
||||
before.sort();
|
||||
after.sort();
|
||||
assert_eq!(before, after);
|
||||
assert_eq!(result.decoded.project, project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_message_value_edits_preserve_field_comments_and_reordering_uses_field_identity() {
|
||||
let source = r#"oa_activity send "Send" {} oa_activity receive "Receive" {}
|
||||
oa_exchange_item data "Data" {}
|
||||
oa_data_field first_value "First" { parent @data; }
|
||||
oa_data_field second_value "Second" { parent @data; }
|
||||
interaction transfer "Transfer" { source @send; target @receive; items [@data]; }
|
||||
oa_scenario conversation "Conversation" { viewpoint OperationalActivity; steps [@message]; }
|
||||
oa_lifeline sender_line "Sender" { parent @conversation; represented @send; }
|
||||
oa_lifeline receiver_line "Receiver" { parent @conversation; represented @receive; }
|
||||
oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
||||
exchange @transfer; sender @sender_line; receiver @receiver_line;
|
||||
values [
|
||||
// first annotation
|
||||
{ field @ // first field proof
|
||||
first_value; expression // first expression proof
|
||||
"first expression"; },
|
||||
// second annotation
|
||||
{ expression "second expression"; field @second_value; }
|
||||
];
|
||||
}; }; }
|
||||
"#;
|
||||
let current = compile(&bundle(
|
||||
"arcadia 3; project p \"P\" { include \"conversation.arcadia\"; }",
|
||||
&[("conversation.arcadia", source)],
|
||||
));
|
||||
let mut project = current.decoded.project.clone();
|
||||
let message = id(&project, "message");
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project.elements.get_mut(&message).unwrap().shared_mut()
|
||||
{
|
||||
values[0].expression = "changed expression".into();
|
||||
} else {
|
||||
panic!("message");
|
||||
}
|
||||
let changed = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
changed.bundle.files["conversation.arcadia"],
|
||||
current.bundle.files["conversation.arcadia"]
|
||||
.replace("\"first expression\"", "\"changed expression\"")
|
||||
);
|
||||
let first = id(&project, "first_value");
|
||||
project.elements.get_mut(&first).unwrap().alias = "renamed_value".into();
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { values, .. },
|
||||
..
|
||||
}) = project.elements.get_mut(&message).unwrap().shared_mut()
|
||||
{
|
||||
values.reverse();
|
||||
}
|
||||
let reordered = update_bundle(&changed, &project, ProjectLimits::default()).unwrap();
|
||||
let after = &reordered.bundle.files["conversation.arcadia"];
|
||||
assert!(after.contains("field @ // first field proof\n renamed_value; expression // first expression proof\n \"changed expression\";"));
|
||||
assert!(
|
||||
after.find("// second annotation").unwrap() < after.find("// first annotation").unwrap()
|
||||
);
|
||||
let mut old_comments = comments(¤t.bundle.files["conversation.arcadia"]);
|
||||
let mut new_comments = comments(after);
|
||||
old_comments.sort();
|
||||
new_comments.sort();
|
||||
assert_eq!(old_comments, new_comments);
|
||||
for (id, location) in ¤t.index.declarations {
|
||||
assert_eq!(reordered.index.declarations[id].path, location.path);
|
||||
}
|
||||
assert_eq!(reordered.decoded.project, project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotations() {
|
||||
let input = bundle(
|
||||
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
|
||||
&[(
|
||||
"types.arcadia",
|
||||
"oa_data_type values \"Values\" { definition { Enumeration { literals [ // enumeration rationale\n ]; }; }; }\n",
|
||||
)],
|
||||
);
|
||||
let empty = compile(&input);
|
||||
let mut project = empty.decoded.project.clone();
|
||||
let values = id(&project, "values");
|
||||
if let Some(SharedBody::DataType { definition }) =
|
||||
project.elements.get_mut(&values).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Enumeration {
|
||||
literals: vec!["Alpha".into(), "Beta".into()],
|
||||
};
|
||||
} else {
|
||||
panic!("enumeration");
|
||||
}
|
||||
let added = update_bundle(&empty, &project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(
|
||||
comments(&added.bundle.files["types.arcadia"]),
|
||||
comments(&empty.bundle.files["types.arcadia"])
|
||||
);
|
||||
let mut annotated = added.bundle.clone();
|
||||
annotated
|
||||
.files
|
||||
.get_mut("types.arcadia")
|
||||
.unwrap()
|
||||
.clone_from(
|
||||
&added.bundle.files["types.arcadia"]
|
||||
.replace("\"Alpha\"", "\"Alpha\" // alpha explanation\n")
|
||||
.replace("\"Beta\"", "\"Beta\" // beta explanation\n"),
|
||||
);
|
||||
let annotated = compile(&annotated);
|
||||
if let Some(SharedBody::DataType { definition }) =
|
||||
project.elements.get_mut(&values).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Enumeration {
|
||||
literals: vec!["Beta".into(), "Gamma".into()],
|
||||
};
|
||||
}
|
||||
let changed = update_bundle(&annotated, &project, ProjectLimits::default()).unwrap();
|
||||
let mut before = comments(&annotated.bundle.files["types.arcadia"]);
|
||||
let mut after = comments(&changed.bundle.files["types.arcadia"]);
|
||||
before.sort();
|
||||
after.sort();
|
||||
assert_eq!(before, after);
|
||||
assert_eq!(changed.decoded.project, project);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user