-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add expr formatting to sway-fmt-v2
#2338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @eureka-cpu, I am taking a quick look and wanted to have a little comment. First of all, this seems really great 🔥, for the tests of comments at #2311, I couldn't add comments with expr test because we didn't have expr formatting back then and the way comment stuff works were duplicating comments next to exprs. With this PR, we should be able to add expr comments as well, so it might be a good idea to add a test_expr_comments with this introduction of expr formatting.
I don't know if you were planning to add such tests but wanted to give a heads-up on that 👍
… projection as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking super clean and readable @eureka-cpu, nice work!
I've left a few nits that would be good to address, but no showstoppers. I'll leave it up to you whether we want to address them in this PR or if we want to handle them in smaller follow-up PRs.
Shape changes
I have a slight hesitation about exposing the formatter's Shape state as done in this PR as I can imagine it might lead to cases where a Format implementation could easily forget to reset the line style or width heuristics at the end of an item's formatting, leading to following items being mysteriously incorrect only on occasion.
We could potentially avoid this with methods on Formatter that scope these temporary shaping changes, e.g.
formatter.with_line_style(new_style, |formatter| {
// Do some formatting with `new_style`.
});
// Automatically revert to original style after scope exited.With an API along these lines, a Format implementation could never accidentally leave the Formatter's inner state in some unexpected state.
All that said, this might require some more significant redesign, and we can investigate diving into this in future issues/PRs. Your existing approach appears to be working so happy to land it!
| assert(balance_test_contract_balance == 3); | ||
| true | ||
| }"#; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a future PR it might be useful to split this test up into (or create more) small tests for each kind of expression, as it might make it easier to spot the cause of a failing test in CI when we inevitably break formatting by mistake in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While splitting to smaller tests we could also introduce a commented version of those new tests, just like we have for other types
Just like:
Line 530 in a23accd
| fn test_enum_comments() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is actually specifically what @mohammadfawaz gave me for formatting method calls, hence why it was used. But I can see that smaller tests would be better, since we may only end up breaking one specific thing like inline method calls.
| }); | ||
|
|
||
| let mut value_pairs_iter = value_pairs.iter().enumerate().peekable(); | ||
| for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I'm just noticing this line might not be quite what we're after. We call value_pairs_iter.clone() here and then use value_pairs_iter.peek() later on during the loop. As a result, the value_pairs_iter.peek() will only ever return the first value which I think is probably not what we want?
To fix this, we could change this line to:
| for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() { | |
| while let Some((var_index, (type_field, comma_token))) = value_pairs_iter.next() { |
Using while let allows us to avoid consuming the iterator upon starting iteration, and as a result is often used when working with peekable iterators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was curious of that, I believe this has been there for some time
| }); | ||
|
|
||
| let mut value_pairs_iter = value_pairs.iter().enumerate().peekable(); | ||
| for (var_index, (type_field, comma_token)) in value_pairs_iter.clone() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here (w.r.t. using while let instead)
| let mut collected_spans = vec![ByteSpan::from(self.register.span())]; | ||
| if let Some(value) = &self.value_opt { | ||
| collected_spans.append(&mut value.leaf_spans()); | ||
| // TODO: determine if we are allowing comments between `:` and expr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we should support comments everywhere that the parser supports them. Perhaps we should open dedicated issues that we can link to in these TODOs so its easy to keep track.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we are supporting this kind of comments as long as it is parsed.
Example
// This can be parsed and supported
let number: /* this number is for counting */ u64 = 10;
// This cannot be parsed and not supported
let number: // this number is for counting u64 = 10;Weird thing is I thought I get rid of these comments with #2416 and it seems like we do not have it currently (here). Would it be possible while rebasing this PR, those come back accidentally?
To summarize we should be currently supporting all of those TODOs and after we decided to support every possible place the comments are deleted as they were no longer relevant 🤔
| let mut collected_spans = vec![ByteSpan::from(self.register.span())]; | ||
| if let Some(ty) = &self.ty_opt { | ||
| collected_spans.append(&mut ty.leaf_spans()); | ||
| // TODO: determine if we are allowing comments between `:` and ty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^
| write!(formatted_code, "{} ", match_token.span().as_str())?; | ||
| value.format(formatted_code, formatter)?; | ||
| MatchBranch::open_curly_brace(formatted_code, formatter)?; | ||
| let branches = branches.clone().into_inner(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed there are quite a lot of places where we clone() in order to call into_inner(), even though we only need the inner type by reference.
In this case, the Braces type has a get method which allows us to use the existing instance directly rather than cloning the whole structure.
It might be worth checking all our uses of .clone().into_inner() for similar cases to this as we can likely avoid quite a bit of unnecessary allocation.
| } | ||
|
|
||
| /// Collects various expr field's ByteSpans. | ||
| fn visit_expr(expr: &Expr) -> Vec<ByteSpan> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function might be better named expr_leaf_spans as we are just matching on the expr to collect leaf spans, not really doing visitor traversal or anything like that.
|
|
||
| // TODO: | ||
| // path, literal, abicast, struct, tuple, parens, block, array, asm, return, if, match, while, func app, index, field projection, tuple field projection, ref, deref, operators, bitwise stuff, comp operators | ||
| // logical operators, reassignment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we open an issue for these and link it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure! I have taken care of a few of them. I believe we would need all of them as tests before this PR can be approved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*at least the horizontal ones
| keywords::CommaToken, punctuated::Punctuated, token::PunctKind, StorageField, TypeField, | ||
| }; | ||
| use sway_types::{Ident, Spanned}; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I think it's good to keep use statements together as it allows rustfmt to automatically order them, but not that important.
kayagokalp
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great 🚀. Left couple of nits regarding some possible rebasing issue, and expanded @mitchmindtree's great review around the usage of while let.
If you would like to merge this and address changes in a separate PR, just ping me and I can happily approve as they are not blocking issues. I am not approving rn just to give you a chance to look to the reviews 🙌
| let mut collected_spans = vec![ByteSpan::from(self.register.span())]; | ||
| if let Some(value) = &self.value_opt { | ||
| collected_spans.append(&mut value.leaf_spans()); | ||
| // TODO: determine if we are allowing comments between `:` and expr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we are supporting this kind of comments as long as it is parsed.
Example
// This can be parsed and supported
let number: /* this number is for counting */ u64 = 10;
// This cannot be parsed and not supported
let number: // this number is for counting u64 = 10;Weird thing is I thought I get rid of these comments with #2416 and it seems like we do not have it currently (here). Would it be possible while rebasing this PR, those come back accidentally?
To summarize we should be currently supporting all of those TODOs and after we decided to support every possible place the comments are deleted as they were no longer relevant 🤔
| let mut collected_spans = Vec::new(); | ||
| for instruction in &self.instructions { | ||
| collected_spans.append(&mut instruction.leaf_spans()); | ||
| // TODO: probably we shouldn't allow for comments in between the instruction and comma since it may/will result in build failure after formatting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^
| let mut collected_spans = vec![ByteSpan::from(self.register.span())]; | ||
| if let Some(ty) = &self.ty_opt { | ||
| collected_spans.append(&mut ty.leaf_spans()); | ||
| // TODO: determine if we are allowing comments between `:` and ty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^
| assert(balance_test_contract_balance == 3); | ||
| true | ||
| }"#; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While splitting to smaller tests we could also introduce a commented version of those new tests, just like we have for other types
Just like:
Line 530 in a23accd
| fn test_enum_comments() { |
| LineStyle::Multiline => { | ||
| writeln!(formatted_code)?; | ||
| let mut value_pairs_iter = self.value_separator_pairs.iter().peekable(); | ||
| for (type_field, comma_token) in value_pairs_iter.clone() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ Same (w.r.t usage of while let)
| match formatter.shape.code_line.line_style { | ||
| LineStyle::Normal => { | ||
| let value_pairs = &self.value_separator_pairs; | ||
| for (type_field, punctuation) in value_pairs.iter() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess #2338 (comment) is also relevant here
| LineStyle::Inline => { | ||
| write!(formatted_code, " ")?; | ||
| let mut value_pairs_iter = self.value_separator_pairs.iter().peekable(); | ||
| for (type_field, punctuation) in value_pairs_iter.clone() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ Same (w.r.t usage of while let)
| let mut collected_spans = vec![ByteSpan::from(self.field_name.span())]; | ||
| if let Some((colon_token, expr)) = &self.expr_opt { | ||
| collected_spans.push(ByteSpan::from(colon_token.span())); | ||
| // TODO: determine if we are allowing comments between `:` and expr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ Same (w.r.t old comment (deleted in #2416) and does not exists currently re-spawning)
sway/sway-fmt-v2/src/utils/expr.rs
Line 596 in a23accd
| collected_spans.push(ByteSpan::from(expr.0.span())); |
kayagokalp
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
I'm curious if this way will solve the problem of having nested formats, for instance in a |
|
I will create some issues around these comments and address them in separate PRs, thanks so much! |
Closes #2300
Closes #992
Closes #2467
Currently not all expressions have vertical handling, but they do have horizontal. There could also be more tests, but it may be a good idea to start getting reviews now since this has grown beyond the scope of what it was originally. This also fixes an issue of accidentally adding single line formatting for non-literal structures. There are also some changes to
Shapethat made multiline formatting easier. I've gone ahead and removed/added features that we weren't/are using.sfv2#2467