Skip to content
Open
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
52 changes: 45 additions & 7 deletions src/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,15 +1762,53 @@ fn rewrite_ty<R: Rewrite>(
result.push_str(&generics_str);
}

// Default keeps today's behavior when there are no bounds, so comments between
// the ident and `where` are still recovered.
let mut span_end_before_where = generics.span.hi();

if let Some(bounds) = generic_bounds_opt {
if !bounds.is_empty() {
// 2 = `: `
let shape = Shape::indented(indent, context.config);
let shape = shape.offset_left(result.len() + 2, span)?;
let type_bounds = bounds
.rewrite_result(context, shape)
.map(|s| format!(": {}", s))?;
result.push_str(&type_bounds);
let item_shape = Shape::indented(indent, context.config);
let shape = item_shape.offset_left(result.len() + 2, span)?;
let type_bounds = bounds.rewrite_result(context, shape)?;

// The bounds rewrite only covers the bounds themselves, so comments
// around the `:` would otherwise be dropped. Recover them on either
// side of the colon, keeping the exact `: ` layout when there are none.
let bounds_lo = bounds[0].span().lo();
let colon_lo = context
.snippet_provider
.span_before(mk_sp(generics.span.hi(), bounds_lo), ":");
let before_colon = mk_sp(generics.span.hi(), colon_lo);
let after_colon = mk_sp(colon_lo + BytePos(1), bounds_lo);

if contains_comment(context.snippet(before_colon)) {
result = combine_strs_with_missing_comments(
context,
&result,
":",
before_colon,
item_shape,
true,
)?;
} else {
result.push(':');
}

if contains_comment(context.snippet(after_colon)) {
result = combine_strs_with_missing_comments(
context,
&result,
&type_bounds,
after_colon,
item_shape,
true,
)?;
} else {
result.push_str(&format!(" {}", type_bounds));
}
span_end_before_where = bounds[bounds.len() - 1].span().hi();
}
}

Expand All @@ -1787,7 +1825,7 @@ fn rewrite_ty<R: Rewrite>(
false,
"=",
None,
generics.span.hi(),
span_end_before_where,
option,
)?;
result.push_str(&before_where_clause_str);
Expand Down
269 changes: 269 additions & 0 deletions tests/source/associated-type-bounds-with-comments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
#![feature(associated_type_defaults)]

// Exhaustive coverage of comments around associated-type bounds, keyed by
// position relative to the `:` / bounds / `where`, comment kind (line,
// block, multi-line line, multi-line block), and context (trait item with
// bounds, trait item without bounds, GAT, impl/free-alias shared path,
// bounds + RHS default). See #6761.
//
// Out of scope: a comment after bounds with no `where` clause at all (#6815)
// — that region has no span computed for it, so it needs separate handling.

// P1: between the ident/generics and the `:`.
trait P1 {
type A // line
: Bound
where
Self: Copy;

type B /* inline */: Bound
where
Self: Copy;

type C
// multi-
// line
: Bound
where
Self: Copy;

type D
/* multi-
* line
*/
: Bound
where
Self: Copy;
}

// P2: between the `:` and the first bound.
trait P2 {
type A: // line
Bound
where
Self: Copy;

type B: /* inline */ Bound
where
Self: Copy;

type C:
// multi-
// line
Bound
where
Self: Copy;

type D:
/* multi-
* line
*/
Bound
where
Self: Copy;
}

// P3: inside the bounds (generic args). Original #6761 repro.
trait P3 {
type A: Iterator<
// line
Item = Self,
>
where
Self: Copy;

type B: Iterator<
/* inline */
Item = Self,
>
where
Self: Copy;

type C: Iterator<
// multi-
// line
Item = Self,
>
where
Self: Copy;

type D: Iterator<
/* multi-
* line
*/
Item = Self,
>
where
Self: Copy;
}

// P4: between two bounds.
trait P4 {
type A: Bound + // line
Bound
where
Self: Copy;

type B: Bound + /* inline */ Bound
where
Self: Copy;

type C: Bound +
// multi-
// line
Bound
where
Self: Copy;

type D: Bound +
/* multi-
* line
*/
Bound
where
Self: Copy;
}

// P5: after the last bound, before `where`. This is the duplication bug.
trait P5 {
type A: Bound
// line
where
Self: Copy;

type B: Bound /* inline */
where
Self: Copy;

type C: Bound
// multi-
// line
where
Self: Copy;

type D: Bound
/* multi-
* line
*/
where
Self: Copy;
}

// P5, same-line trailing comment: the reviewer's exact input on #7014.
// rustfmt moves this onto its own line. That is `rewrite_where_keyword`'s
// shared behavior for every item kind (see tests/target/issue-3194.rs for
// struct/enum and type-alias-where-clauses-with-comments.rs for aliases),
// not something this fix introduces. Pinned here so the behavior is explicit.
trait P5SameLine {
type A: Iterator<Item = u8> // trailing
where
Self: Copy;

type B: Bound // trailing
where
Self: Copy;

type C: Bound /* trailing */
where
Self: Copy;
}

// P5, no bounds: exercises the preserved `generics.span.hi()` default.
trait P5NoBounds {
type A
// line
where
Self: Copy;

type B /* inline */
where
Self: Copy;

type C
// multi-
// line
where
Self: Copy;

type D
/* multi-
* line
*/
where
Self: Copy;
}

// Width: a colon comment must not wrap while the line still fits in
// max_width. `combine_strs_with_missing_comments` already measures the whole
// prefix, so it must be given the item shape, not the bounds-offset shape,
// or the prefix is charged twice and the line breaks early.
trait ColonCommentWidth {
type AssociatedTypeWithLongName: /* comment */ SomeModeratelyLongTraitName
where
Self: Copy;
}

// P6: after the `where` keyword, before the first predicate.
trait P6 {
type A: Bound
where
// line
Self: Copy;

type B: Bound
where
/* inline */
Self: Copy;

type C: Bound
where
// multi-
// line
Self: Copy;

type D: Bound
where
/* multi-
* line
*/
Self: Copy;
}

// GAT with bounds: non-empty `generics.span`.
trait GatP5 {
type A<U>: Bound
// line
where
U: Copy;

type B<U>: Bound /* inline */
where
U: Copy;
}

// Impl associated type: shared rewrite_ty/where-clause path.
impl ImplTarget for S {
type A<U>
// line, impl assoc type
where
U: Copy,
= Vec<U>;
}

// Free type alias: shared rewrite_ty/where-clause path.
type Free<U>
// line, free alias
where
U: Copy,
= Vec<U>;

// Bounds + RHS default, with comments (was Case 4, previously untested).
trait Rhs {
type A: Bound = () // line
where
Self: Copy;

type B: Bound /* inline */ = ()
where
Self: Copy;
}
9 changes: 9 additions & 0 deletions tests/source/issue-6761.rs

@ytmimi ytmimi Aug 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These test cases are a good start, but I'd like them to be more exhaustive. We should make sure that we're testing both // line comments and /* inline comments */ in all of these positions. And we should make sure that we're testing comments that span multiple lines.

For example,

// multi-
// line
// comments

/* multi-
 * line
 * comments
 * /

View changes since the review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I expanded the coverage into a new associated-type-bounds-with-comments.rs fixture.

For each position around the associated-type bounds and where clause, it now covers:

  • // line comments
  • /* inline */ block comments
  • multi-line // comments
  • multi-line /* ... */ comments

The covered positions are before :, after :, inside generic bounds, between bounds, after the final bound before where, and after where. The fixture also retains targeted cases for the no-bounds fallback, GATs, impl/free-alias paths, and associated-type defaults with an RHS.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Case 1: exact issue repro (comment inside bounds + before-where clause).
pub trait Trait {
type I: Iterator<
// This is an item
Item = Self,
>
where
Self: Copy;
}
Loading