Skip to content
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

Handle comments between Trait Generics and Bounds #4666

Open
wants to merge 4 commits into
base: rustfmt-2.0.0-rc.2
Choose a base branch
from

Conversation

davidBar-On
Copy link
Contributor

@davidBar-On davidBar-On commented Jan 22, 2021

Add handling of comments between Trait Generics and Bounds by using rewrite_assign_rhs_with_comments() (one of the cases identified in #4626 discussion).

Also added workaround for an issue that a comment before the Ident caused code duplication. The workaround is to use the original code in this case.

The PR also partly enhance the resolution of #2055.

Comment on lines 1212 to 1215
let pre_block_span = if !generics.where_clause.predicates.is_empty() {
mk_sp(generics.where_clause.span.hi(), item.span.hi())
} else if !generic_bounds.is_empty() {
mk_sp(generic_bounds.last().unwrap().span().hi(), item.span.hi())
Copy link
Member

Choose a reason for hiding this comment

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

This would conflict with #4711. I'm not sure there's a real case where the generic_bounds would be empty while the collection on generics would not (or vice versa), but either way, I'd think we could just use the end of where_clause span because I believe the parser advances this to desired position in the presence of any generics regardless.

Probably worth checking to confirm though.

Side note, if the span differences necessitate keeping these checks and separate calcs, that instead of checking is_empty and then having to call last and unwrap, an else if let Some(last) = generic_bounds.last() { could be used to express it a bit more succinctly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

... I believe the parser advances this to desired position in the presence of any generics regardless

The item.span is not advanced, but found that a WhereClause is always available, even when a where clause is not available in the code. Therefore, removed all if conditions and changed to use generics.where_clause.span.hi() unconditionally. (Also rebased.)

Copy link
Member

Choose a reason for hiding this comment

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

The item.span is not advanced, but found that a WhereClause is always available

Yes I am aware.

The spans on the AST nodes we're working with are immutable for practical purposes, and the span of the item will always cover the portions it's supposed to cover.

I was referring to the process within the parser that sets those spans, and what it ends up setting as the span for the where_clause in the various cases of source input (no generics at all, generic params with no where clause, where clause with no predicates, and where clause with predicates).

I was just saying that I wasn't positive off hand where the span position of the where_clause is relative to the broader item span in those various scenarios, and that if we're going to use it unconditionally then we should validate to make sure that span will always have the hi end be placed sufficiently far enough so that we can be sure we'd avoid speciously double counting comments which had actually been handled already.

Copy link
Member

@calebcartwright calebcartwright left a comment

Choose a reason for hiding this comment

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

Left some additional comments inline

if contains_comment(context.snippet(mk_sp(item.span.lo(), generics.span.lo()))) {
return None;
}

Copy link
Member

Choose a reason for hiding this comment

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

Nit: if we're going to have an early bail (which is a good idea btw!) then i think we should adjust this a bit.

The inline comment hi end is the beginning of generics, not the ident so if we can handle any comments between the ident and wherever the generics begin then let's reduce the end of this span we're using for the early bail.

Additionally, we can shift this check up front, as there's really no need for the allocation and formatting of the visibility and friends if we're going to have to bail out down here anyway

Comment on lines 1146 to 1153
// FIXME(#2055): rustfmt fails to format when there are comments within trait bounds.
if !generic_bounds.is_empty() {
let ident_hi = context
.snippet_provider
.span_after(item.span, &item.ident.as_str());
let bound_lo = generic_bounds.first().unwrap().span().lo();
let bound_hi = generic_bounds.last().unwrap().span().hi();
let snippet = context.snippet(mk_sp(ident_hi, bound_hi));
let snippet = context.snippet(mk_sp(bound_lo, bound_hi));
if contains_comment(snippet) {
return None;
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if this is true/needed anymore given #4474 (and the fix/adjustment in #4653). I think that currently (with the inclusion of the correction to the pre-block span derivation) we can already handle any and all comments to the right of the colon following the ident.

I think the remaining gaps are really any such comments to the left

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems that you are correct and comments whiting that generics bounds are handled properly. Before I make the change, there is one change to 2015 edition test case output that I want to make sure is acceptable. The following code:

pub trait Foo:
// A and C
A + C
// and B
+ B
{}

Will now be formatted as (same as for the other editions):

pub trait Foo:
    // A and C
    A
        + C
        // and B
        + B
{
}

Is this o.k. or the test for comments within the generics bound should still be done when edition is set to 2015?

Copy link
Member

Choose a reason for hiding this comment

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

The edition really only drives parsing, and shouldn't have any impact on formatting. I wonder if there's a separate bug in there 🤔

What happens if you remove that leading // A and C comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What happens if you remove that leading // A and C comment?

You are right that removing the // A and C comment changes the indentation to:

pub trait Foo:
    A
    + C
    // and B
    + B
{
}

I wonder if there's a separate bug in there.

There seem to be a general a problem in rewrite_assign_rhs_with_comments(). The same issue is seen in the following assignments, where an extra indentation is added because of a comment the the 2nd and following lines of the expression (the long names are because adding the // and B comment into the expressions cause using the original code):

fn main () {
	x =
	Aaaaaaaaaaaaaaaa   +    Cccccccccccccccccccccccccccc
          +    Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ;
	y =     /* A and C */
	Aaaaaaaaaaaaaaaa   +    Cccccccccccccccccccccccccccc
          +    Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ;
	z =     
	/* A and C */
	Aaaaaaaaaaaaaaaa   +    Cccccccccccccccccccccccccccc
          +    Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ;
}

is formatted as:

fn main() {
    x = Aaaaaaaaaaaaaaaa
        + Cccccccccccccccccccccccccccc
        + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
    y = /* A and C */
        Aaaaaaaaaaaaaaaa
            + Cccccccccccccccccccccccccccc
            + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
    z =
        /* A and C */
        Aaaaaaaaaaaaaaaa
            + Cccccccccccccccccccccccccccc
            + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
}

The problem is caused by the following shape adjustment that should probably be done only if the formatted comment is not immediately followed by newline:

rustfmt/src/formatting/expr.rs

Lines 1998 to 2002 in e10d2d7

let shape = if contains_comment {
shape.block_left(context.config.tab_spaces())?
} else {
shape
};

Should I try to fix this issue in this PR, or for now the test case expected target should just be changed? (Note that the test case output will be changed in any case, since now it is using the original code and with this PR the code is formatted.)

Copy link
Member

Choose a reason for hiding this comment

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

The same issue is seen in the following assignments, where an extra indentation is added because of a comment the the 2nd and following lines of the expression

Well, not quite. The former are bounds on a trait whereas your example are binop expressions, and these are governed by different formatting rules.

https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/items.md#traits vs https://github.com/rust-dev-tools/fmt-rfcs/blob/master/guide/expressions.md#binary-operations

As such the only issue/non-compliance with the Style Guide is on the traits, so I think you'll want to try to address that in the same PR since both that fix and the originally discussed changes here would be needed in combination (neither would be mergeable in isolation)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The former are bounds on a trait whereas your example are binop expressions, and these are governed by different formatting rules.

I don't see a reference in the guide to the formatting output I was referring to, so I will try to clarify what I meant. I expected that the y assignment in the example above will be similar to the x assignment and to the formatting example in the guide:

    y = /* A and C */
        Aaaaaaaaaaaaaaaa
        + Cccccccccccccccccccccccccccc
        + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;

However, because of the comment the 3rd and 4th lines are formatted with additional indentation:

    y = /* A and C */
        Aaaaaaaaaaaaaaaa
            + Cccccccccccccccccccccccccccc
            + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;

Is this the expected output?

Copy link
Member

Choose a reason for hiding this comment

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

I expected that the y assignment in the example above will be similar to the x assignment and to the formatting example in the guide

What I'm saying is that you've changed to the (off)topic discussion of binary expressions which is not relevant for the formatting of trait bounds, and additionally that your expectation about binary expression formatting is incorrect.

From the binary expression section of the guide (emphasis mine):

If line-breaking, put the operator on a new line and block indent. Put each sub-expression on its own line

The block indents in the binary expressions are happening because they are supposed to be there, the only impact the comment between the assignment operator and the rhs is the placement of the first operand. If another condition (such as a comment) results in the opening of the binary expression not being able to be placed on the same line as the lhs/assignment operator, then that first operand of the bin expr has to be block indented itself, but the other subexpr(s) still have to be block indented relative to that first operand.

It is merely a coincidence in your first snippet that the first operand happens to be in the same column position as the block indentation, due to the single character variable name (x + = + two spaces). If you were to use a longer var name, have a let, etc. you will see that the alignment is different:

e.g. try formatting these:

fn main() {
    let x = Aaaaaaaaaaaaaaaa
        + Cccccccccccccccccccccccccccc
        + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
        
        foo_bar = Aaaaaaaaaaaaaaaa
        + Cccccccccccccccccccccccccccc
        + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb;
}

Hopefully that helps, but if you have additional thoughts/questions/doubts about the binary expression formatting, then I'd encourage opening a separate issue to avoid taking this thread any farther away from the issue at hand, namely the extra, incorrect additional block indent that's added to the trait bounds when there's a comment preceding the first bound.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Submitted changes which I hope satisfy the comments requirements.

Hopefully that helps, but if you have additional thoughts/questions/doubts about the binary expression formatting, then I'd encourage opening a separate issue

Yes, I understand now. Thanks for the detailed explanation. I do have some minor comments, but its not worth further discussion, especially considering backward compatibility.

What I'm saying is that you've changed to the (off)topic discussion of binary expressions which is not relevant for the formatting of trait bounds

I know it was (of)topic but I thought that a common change for both traits and binop is required. I now added a separate function to handle trait rhs with comments.

@@ -0,0 +1,70 @@
// Based on issue #2055:
Copy link
Member

Choose a reason for hiding this comment

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

Given the updates to the pre block span, we'd want to have tests here that included a mix of some other trait cases, including some with where clauses

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we'd want to have tests here that included a mix of some other trait cases, including some with where clauses

Added some test cases mainly with where clause.

@davidBar-On davidBar-On force-pushed the lhs-to-rhs-bewteen-comments-trait branch from af74fbb to cc0daad Compare March 22, 2021 07:37
Comment on lines 2014 to 2042
pub(crate) fn rewrite_trait_rhs_with_comments<S: Into<String>, R: Rewrite>(
context: &RewriteContext<'_>,
lhs: S,
ex: &R,
shape: Shape,
rhs_tactics: RhsTactics,
between_span: Span,
allow_extend: bool,
) -> Option<String> {
let lhs = lhs.into();
let contains_comment = contains_comment(context.snippet(between_span));

let rhs = rewrite_rhs_expr(context, &lhs, ex, shape, rhs_tactics, ":")?;

if contains_comment {
let rhs = rhs.trim_start();
combine_strs_with_missing_comments(
context,
&lhs,
&rhs,
between_span,
shape.block_left(context.config.tab_spaces())?,
allow_extend,
)
} else {
Some(lhs + &rhs)
}
}

Copy link
Member

Choose a reason for hiding this comment

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

I think the overall idea here is good, but think we're doing this in the wrong place in the codebase. Traits are by definition Items, not Expressions, and by design (encapsulation, etc.) we'd only ever need to call this from one place: the corresponding formatting flow for traits in items.rs.

As such, this function feels quite out of place here in expr.rs and the logic would be best placed back in items.

Additionally (regarding changes above this one), and a bit of a nit, it's quite reasonable to have one of these rewrite functions allow the caller to specify the separator, but that doesn't mean we can't also have a helper function that avoids the duplicative calls that each have to specify the assignment operator as the separator (which would be introduced here)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As such, this function feels quite out of place here in expr.rs and the logic would be best placed back in items.

Moved rewrite_trait_rhs_with_comments to items.rs.

... have a helper function that avoids the duplicative calls that each have to specify the assignment operator as the separator

Added a helper function rewrite_assign_rhs_expr that specifies the '=' separator.

@davidBar-On davidBar-On force-pushed the lhs-to-rhs-bewteen-comments-trait branch from cf551a3 to 2dda3da Compare March 30, 2021 09:19
@davidBar-On davidBar-On force-pushed the lhs-to-rhs-bewteen-comments-trait branch from 2dda3da to e4ab01e Compare March 30, 2021 09:35
@calebcartwright
Copy link
Member

Thanks for the updates! I may have a very minor nit or two as a final review pass, but I think we're otherwise in good shape.

I want to think on this a bit longer to make sure we're not overlooking any scenarios. In cases like these it's possible that folks may feel like after upgrading rustfmt that it is "changed" its formatting, but really rustfmt is just finally able to format these trait/comment scenarios. It's definitely a fix we want/need, but it's one we need to be sure we've got right/fully covered up front (because we'll be limited in our ability to change it afterwards)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p-low pr-targeting-2.0 This PR is targeting the 2.0 branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants