Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1197,4 +1197,57 @@ fn foo() {
"#,
);
}

#[test]
fn regression_issue_21020() {
check_assist(
convert_tuple_struct_to_named_struct,
r#"
pub struct S$0(pub ());

trait T {
fn id(&self) -> usize;
}

trait T2 {
fn foo(&self) -> usize;
}

impl T for S {
fn id(&self) -> usize {
self.0.len()
}
}

impl T2 for S {
fn foo(&self) -> usize {
self.0.len()
}
}
"#,
r#"
pub struct S { pub field1: () }

trait T {
fn id(&self) -> usize;
}

trait T2 {
fn foo(&self) -> usize;
}

impl T for S {
fn id(&self) -> usize {
self.field1.len()
}
}

impl T2 for S {
fn foo(&self) -> usize {
self.field1.len()
}
}
"#,
);
}
}
54 changes: 54 additions & 0 deletions crates/syntax/src/syntax_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,58 @@ mod tests {
let expect = expect![["fn it() {\n \n}"]];
expect.assert_eq(&edit.new_root.to_string());
}

#[test]
fn test_more_times_replace_node_to_mutable_token() {
let arg_list =
make::arg_list([make::expr_literal("1").into(), make::expr_literal("2").into()]);

let mut editor = SyntaxEditor::new(arg_list.syntax().clone());
let target_expr = make::token(parser::SyntaxKind::UNDERSCORE);

for arg in arg_list.args() {
editor.replace(arg.syntax(), &target_expr);
}

let edit = editor.finish();

let expect = expect![["(_, _)"]];
expect.assert_eq(&edit.new_root.to_string());
}

#[test]
fn test_more_times_replace_node_to_mutable() {
let arg_list =
make::arg_list([make::expr_literal("1").into(), make::expr_literal("2").into()]);

let mut editor = SyntaxEditor::new(arg_list.syntax().clone());
let target_expr = make::expr_literal("3").clone_for_update();

for arg in arg_list.args() {
editor.replace(arg.syntax(), target_expr.syntax());
}

let edit = editor.finish();

let expect = expect![["(3, 3)"]];
expect.assert_eq(&edit.new_root.to_string());
}

#[test]
fn test_more_times_insert_node_to_mutable() {
let arg_list =
make::arg_list([make::expr_literal("1").into(), make::expr_literal("2").into()]);

let mut editor = SyntaxEditor::new(arg_list.syntax().clone());
let target_expr = make::ext::expr_unit().clone_for_update();

for arg in arg_list.args() {
editor.insert(Position::before(arg.syntax()), target_expr.syntax());
}

let edit = editor.finish();

let expect = expect![["(()1, ()2)"]];
expect.assert_eq(&edit.new_root.to_string());
}
}
41 changes: 41 additions & 0 deletions crates/syntax/src/syntax_editor/edit_algo.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dedup logic looks correct to me, but I’m not sure whether inserting or replacing with the same syntax node multiple times is part of the intended design. Should we automatically handle this by cloning the duplicated node (as done here), or should we reject duplicates and require the caller to handle it instead?
@DropDemBits, I’d appreciate your thoughts on this 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, it's been a year but I managed to recollect enough context to form an opinion 😆

The current SyntaxEditor design admits that SyntaxNodes do have a form of identity which at the moment is tracked by SyntaxNodes sharing the same NodeData1. At the same time however, the implementation detail being mutable syntax trees necessarily means that shared nodes get mutated (causing this issue), while using clone_subtree breaks this identity.

For now, I am okay with letting this through to fix this issue, but I've opened #21155 for myself to fix the broader issue.

Footnotes

  1. This doesn't necessarily lock us in to having SyntaxNodes be reference counted, just that they need to carry through some identity token when we need it, e.g. some u32 derived from a global AtomicU32 counter. In fact, lifting the concept of node identity into rowan proper should let us remove SyntaxMappings, annoying as they are!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huge thanks for your clear explanation!

Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
// Map change targets to the correct syntax nodes
let tree_mutator = TreeMutator::new(&root);
let mut changed_elements = vec![];
let mut changed_elements_set = rustc_hash::FxHashSet::default();
let mut deduplicate_node = |node_or_token: &mut SyntaxElement| {
let node;
let node = match node_or_token {
SyntaxElement::Token(token) => match token.parent() {
None => return,
Some(parent) => {
node = parent;
&node
}
},
SyntaxElement::Node(node) => node,
};
if changed_elements_set.contains(node) {
let new_node = node.clone_subtree().clone_for_update();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unfortunately does break node identity at the moment, but it doesn't seem like there's any test depending on it at the moment. There's also not really a good way to fix this at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also be broadcasting source nodes for dependent changes, but that's trickier because we still need to preserve the mapping in that case. Again, not really a good way to fix that at the moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, looks like I've merged without enough checks 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fiiiine 😆

match node_or_token {
SyntaxElement::Node(node) => *node = new_node,
SyntaxElement::Token(token) => {
*token = new_node
.children_with_tokens()
.filter_map(SyntaxElement::into_token)
.find(|it| it.kind() == token.kind() && it.text() == token.text())
.unwrap();
}
}
} else {
changed_elements_set.insert(node.clone());
}
};

for index in independent_changes {
match &mut changes[index as usize] {
Expand Down Expand Up @@ -180,6 +209,18 @@ pub(super) fn apply_edits(editor: SyntaxEditor) -> SyntaxEdit {
}
}

match &mut changes[index as usize] {
Change::Insert(_, element) | Change::Replace(_, Some(element)) => {
deduplicate_node(element);
}
Change::InsertAll(_, elements)
| Change::ReplaceWithMany(_, elements)
| Change::ReplaceAll(_, elements) => {
elements.iter_mut().for_each(&mut deduplicate_node);
}
Change::Replace(_, None) => (),
}

// Collect changed elements
match &changes[index as usize] {
Change::Insert(_, element) => changed_elements.push(element.clone()),
Expand Down