xref https://github.com/golang/go/issues/56592

What version of Go are you using (go version)?

$ gotip version
go version devel go1.21-f00c54146c Mon Apr 10 22:52:22 2023 +0000 linux/amd64

Does this issue reproduce with the latest release?

This tests a change made on tip.

What operating system and processor architecture are you using (go env)?

NA

What did you do?

Given this file:

// Package p is a package.
//
//magic:true
package p

// T is a type
//
//magic:true
type T struct {
    // Field is a field
    //magic:true
    Field int
}

go 1.20 go doc shows:

$ go doc --all .
package p // import "hockin.org/p"

Package p is a package.

TYPES

type T struct {
    // Field is a field
    //magic:true
    Field int
}
    T is a type

Note the directive line is present. gotip doc says:

$ gotip doc --all .
package p // import "hockin.org/p"

Package p is a package.

TYPES

type T struct {
    // Field is a field

    Field int
}
    T is a type

The directive is gone (yay) but there's now a blank line. @ianlancetaylor explains it in https://github.com/golang/go/issues/56592#issuecomment-1502475206 and acknowledges that it is imperfect (but better than before).

Comment From: thockin

Following up, this still exists in go 1.24. Additionally, pkgsite does not even bother to elide directives in comments on struct fields.

Comment From: thockin

Digging a bit, to understand the scope:

go/ast.isDirective() is called from go/ast.CommentGroup.Text() from main.trimUnexportedFields() which specifically references https://github.com/golang/go/issues/56592.

cmd/doc/pkg.go :: func Package.emit() calls format.Node(), which calls config.Fprint() (go/printer.Config.Fprint()). That eventually calls go/printer.printer.printNode() -> printer.decl() -> printer.genDecl() -> printer.spec() -> printer.expr() -> printer.expr1() -> printer.fieldList(). At that point we can see that the directive has already been stripped out (as expected, given the above).

Tracking more, we eventually hit printer.print() which calls printer.flush() to line up for the next node, which includes printing comments associated with that node. Or that's what I would assume. In fact .intersperseComments() says "all comments that appear before the next token". The comment is not "linked to" the token any more, despite that several callframes back (fieldList()) had an ast.Field with a Name and a Doc. We tore the information apart, stuffed it into p in different places and then lost track of the fact that they go together. I think it can't tell between:

type T struct {
  // floating comment

  F int
}

and:

type T struct {
  // attached comment
  //some:directive
  F int
}

Oh, but the floating comment isn't represented in go doc, so we don't need to consider that case. I think the comment HAS TO BE attached to the current node. So in this code (printer.print())

  1000: next := p.pos // estimated/accurate position of next item
  1001: wroteNewline, droppedFF := p.flush(next, p.lastTok)
  1002: 
  1003: // intersperse extra newlines if present in the source and
  1004: // if they don't cause extra semicolons (don't do this in
  1005: // flush as it will cause extra newlines at the end of a file)
  1006: if !p.impliedSemi {
  1007:      n := nlimit(next.Line - p.pos.Line)
  1008:     // don't exceed maxNewlines if we already wrote one
  1009:     if wroteNewline && n == maxNewlines {
  1010:         n = maxNewlines - 1
  1011:     }
  1012:     if n > 0 {
  1013:         ch := byte('\n')
  1014:         if droppedFF {
  1015:             ch = '\f' // use formfeed since we dropped one before
  1016:         }
  1017:         p.writeByte(ch, n)
  1018:         impliedSemi = false
  1019:     }

Could we assume that if wroteNewline == true and next.Line != p.pos.Line that the pos is lying, and increment the current position (p.pos)? It seems to work for this case but fails tests. Maybe only for some node types this is true?