英文:
Go parser not detecting Doc comments on struct type
问题
我正在尝试使用Go的parser和ast包来读取结构体类型上关联的文档注释。在这个示例中,代码简单地将自身用作源代码。
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
// FirstType docs
type FirstType struct {
// FirstMember docs
FirstMember string
}
// SecondType docs
type SecondType struct {
// SecondMember docs
SecondMember string
}
// Main docs
func main() {
fset := token.NewFileSet() // positions are relative to fset
d, err := parser.ParseDir(fset, "./", nil, parser.ParseComments)
if err != nil {
fmt.Println(err)
return
}
for _, f := range d {
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
fmt.Printf("%s:\tFuncDecl %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc)
case *ast.TypeSpec:
fmt.Printf("%s:\tTypeSpec %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc)
case *ast.Field:
fmt.Printf("%s:\tField %s\t%s\n", fset.Position(n.Pos()), x.Names, x.Doc)
}
return true
})
}
}
函数和字段的注释文档可以正常输出,但是奇怪的是找不到"FirstType docs"和"SecondType docs"。我漏掉了什么?Go版本是1.1.2。
(要运行上面的代码,请将其保存为main.go文件,然后运行go run main.go
)
英文:
I am trying to read the assocated Doc comments on a struct type using Go’s parser and ast packages. In this example, the code simply uses itself as the source.
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
// FirstType docs
type FirstType struct {
// FirstMember docs
FirstMember string
}
// SecondType docs
type SecondType struct {
// SecondMember docs
SecondMember string
}
// Main docs
func main() {
fset := token.NewFileSet() // positions are relative to fset
d, err := parser.ParseDir(fset, "./", nil, parser.ParseComments)
if err != nil {
fmt.Println(err)
return
}
for _, f := range d {
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
fmt.Printf("%s:\tFuncDecl %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc)
case *ast.TypeSpec:
fmt.Printf("%s:\tTypeSpec %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc)
case *ast.Field:
fmt.Printf("%s:\tField %s\t%s\n", fset.Position(n.Pos()), x.Names, x.Doc)
}
return true
})
}
}
The comment docs for the func and fields are output no problem, but for some reason the ‘FirstType docs’ and ‘SecondType docs’ are nowhere to be found. What am I missing? Go version is 1.1.2.
(To run the above, save it into a main.go file, and go run main.go
)
答案1
得分: 23
很好的问题!
通过查看go/doc
的源代码,我们可以看到它在readType
函数中处理了相同的情况。在那里,它说:
func (r *reader) readType(decl *ast.GenDecl, spec *ast.TypeSpec) {
...
// 计算文档
doc := spec.Doc
spec.Doc = nil // doc已使用 - 从AST中删除
if doc == nil {
// 没有与规范关联的文档,如果有的话,使用声明文档
doc = decl.Doc
}
...
特别注意它如何处理AST没有附加到TypeSpec的文档的情况。为了做到这一点,它回退到了GenDecl
。这给了我们一个线索,关于如何直接使用AST解析结构体的文档注释。将问题代码中的for循环适应添加一个*ast.GenDecl
的情况:
for _, f := range d {
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
fmt.Printf("%s:\tFuncDecl %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc.Text())
case *ast.TypeSpec:
fmt.Printf("%s:\tTypeSpec %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc.Text())
case *ast.Field:
fmt.Printf("%s:\tField %s\t%s\n", fset.Position(n.Pos()), x.Names, x.Doc.Text())
case *ast.GenDecl:
fmt.Printf("%s:\tGenDecl %s\n", fset.Position(n.Pos()), x.Doc.Text())
}
return true
})
}
运行这段代码,我们得到了:
main.go:3:1: GenDecl %!s(*ast.CommentGroup=<nil>)
main.go:11:1: GenDecl &{[%!s(*ast.Comment=&{69 // FirstType docs})]}
main.go:11:6: TypeSpec FirstType %!s(*ast.CommentGroup=<nil>)
main.go:13:2: Field [FirstMember] &{[%!s(*ast.Comment=&{112 // FirstMember docs})]}
main.go:17:1: GenDecl &{[%!s(*ast.Comment=&{155 // SecondType docs})]}
main.go:17:6: TypeSpec SecondType %!s(*ast.CommentGroup=<nil>)
main.go:19:2: Field [SecondMember] &{[%!s(*ast.Comment=&{200 // SecondMember docs})]}
main.go:23:1: FuncDecl main &{[%!s(*ast.Comment=&{245 // Main docs})]}
main.go:33:23: Field [n] %!s(*ast.CommentGroup=<nil>)
main.go:33:35: Field [] %!s(*ast.CommentGroup=<nil>)
嘿!
我们打印出了丢失的FirstType docs
和SecondType docs
!但是这还不令人满意。为什么文档没有附加到TypeSpec
上?go/doc/reader.go
文件为了解决这个问题而采取了非常复杂的方法,实际上生成了一个假的GenDecl
,并将其传递给前面提到的readType
函数,如果结构体声明没有相关的文档!
fake := &ast.GenDecl{
Doc: d.Doc,
TokPos: s.Pos(),
Tok: token.TYPE,
Specs: []ast.Spec{s},
}
但是为什么要这样做?
想象一下,我们稍微改变了问题中的代码中的类型定义(这样定义结构体并不常见,但仍然是有效的Go代码):
// This documents FirstType and SecondType together
type (
// FirstType docs
FirstType struct {
// FirstMember docs
FirstMember string
}
// SecondType docs
SecondType struct {
// SecondMember docs
SecondMember string
}
)
运行代码(包括ast.GenDecl
的情况),我们得到:
main.go:3:1: GenDecl %!s(*ast.CommentGroup=<nil>)
main.go:11:1: GenDecl &{[%!s(*ast.Comment=&{69 // This documents FirstType and SecondType together})]}
main.go:13:2: TypeSpec FirstType &{[%!s(*ast.Comment=&{129 // FirstType docs})]}
main.go:15:3: Field [FirstMember] &{[%!s(*ast.Comment=&{169 // FirstMember docs})]}
main.go:19:2: TypeSpec SecondType &{[%!s(*ast.Comment=&{215 // SecondType docs})]}
main.go:21:3: Field [SecondMember] &{[%!s(*ast.Comment=&{257 // SecondMember docs})]}
main.go:26:1: FuncDecl main &{[%!s(*ast.Comment=&{306 // Main docs})]}
main.go:36:23: Field [n] %!s(*ast.CommentGroup=<nil>)
main.go:36:35: Field [] %!s(*ast.CommentGroup=<nil>)
没错
现在结构体类型定义有了它们的文档,GenDecl
也有了自己的文档。在问题中的第一种情况下,文档附加到了GenDecl
上,因为AST将括号版本的类型定义的各个结构体类型定义视为“缩写”,并希望以相同的方式处理所有定义,无论它们是否分组。对于变量定义也会发生同样的情况,例如:
// some general docs
var (
// v docs
v int
// v2 docs
v2 string
)
因此,如果您希望使用纯AST解析注释,您需要意识到它是如何工作的。但是,正如@mjibson建议的那样,首选的方法是使用go/doc
。祝你好运!
英文:
Great question!
Looking at the source code of go/doc
, we can see that it has to deal with this same case in readType
function. There, it says:
324 func (r *reader) readType(decl *ast.GenDecl, spec *ast.TypeSpec) {
...
334 // compute documentation
335 doc := spec.Doc
336 spec.Doc = nil // doc consumed - remove from AST
337 if doc == nil {
338 // no doc associated with the spec, use the declaration doc, if any
339 doc = decl.Doc
340 }
...
Notice in particular how it needs to deal with the case where the AST does not have a doc attached to the TypeSpec. To do this, it falls back on the GenDecl
. This gives us a clue as to how we might use the AST directly to parse doc comments for structs. Adapting the for loop in the question code to add a case for *ast.GenDecl
:
for _, f := range d {
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.FuncDecl:
fmt.Printf("%s:\tFuncDecl %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc.Text())
case *ast.TypeSpec:
fmt.Printf("%s:\tTypeSpec %s\t%s\n", fset.Position(n.Pos()), x.Name, x.Doc.Text())
case *ast.Field:
fmt.Printf("%s:\tField %s\t%s\n", fset.Position(n.Pos()), x.Names, x.Doc.Text())
case *ast.GenDecl:
fmt.Printf("%s:\tGenDecl %s\n", fset.Position(n.Pos()), x.Doc.Text())
}
return true
})
}
Running this gives us:
main.go:3:1: GenDecl %!s(*ast.CommentGroup=<nil>)
main.go:11:1: GenDecl &{[%!s(*ast.Comment=&{69 // FirstType docs})]}
main.go:11:6: TypeSpec FirstType %!s(*ast.CommentGroup=<nil>)
main.go:13:2: Field [FirstMember] &{[%!s(*ast.Comment=&{112 // FirstMember docs})]}
main.go:17:1: GenDecl &{[%!s(*ast.Comment=&{155 // SecondType docs})]}
main.go:17:6: TypeSpec SecondType %!s(*ast.CommentGroup=<nil>)
main.go:19:2: Field [SecondMember] &{[%!s(*ast.Comment=&{200 // SecondMember docs})]}
main.go:23:1: FuncDecl main &{[%!s(*ast.Comment=&{245 // Main docs})]}
main.go:33:23: Field [n] %!s(*ast.CommentGroup=<nil>)
main.go:33:35: Field [] %!s(*ast.CommentGroup=<nil>)
And, hey!
We've printed out the long-lost FirstType docs
and SecondType docs
! But this is unsatisfactory. Why is the doc not attached to the TypeSpec
? The go/doc/reader.go
file goes to extraordinary lengths to circumvent this issue, actually generating a fake GenDecl
and passing it to the readType
function mentioned earlier, if there is no documentation associated with the struct declaration!
503 fake := &ast.GenDecl{
504 Doc: d.Doc,
505 // don't use the existing TokPos because it
506 // will lead to the wrong selection range for
507 // the fake declaration if there are more
508 // than one type in the group (this affects
509 // src/cmd/godoc/godoc.go's posLink_urlFunc)
510 TokPos: s.Pos(),
511 Tok: token.TYPE,
512 Specs: []ast.Spec{s},
513 }
But why all this?
Imagine we changed the type definitions from code in the question slightly (defining structs like this is not common, but still valid Go):
// This documents FirstType and SecondType together
type (
// FirstType docs
FirstType struct {
// FirstMember docs
FirstMember string
}
// SecondType docs
SecondType struct {
// SecondMember docs
SecondMember string
}
)
Run the code (including the case for ast.GenDecl
) and we get:
main.go:3:1: GenDecl %!s(*ast.CommentGroup=<nil>)
main.go:11:1: GenDecl &{[%!s(*ast.Comment=&{69 // This documents FirstType and SecondType together})]}
main.go:13:2: TypeSpec FirstType &{[%!s(*ast.Comment=&{129 // FirstType docs})]}
main.go:15:3: Field [FirstMember] &{[%!s(*ast.Comment=&{169 // FirstMember docs})]}
main.go:19:2: TypeSpec SecondType &{[%!s(*ast.Comment=&{215 // SecondType docs})]}
main.go:21:3: Field [SecondMember] &{[%!s(*ast.Comment=&{257 // SecondMember docs})]}
main.go:26:1: FuncDecl main &{[%!s(*ast.Comment=&{306 // Main docs})]}
main.go:36:23: Field [n] %!s(*ast.CommentGroup=<nil>)
main.go:36:35: Field [] %!s(*ast.CommentGroup=<nil>)
That's right
Now the struct type definitions have their docs, and the GenDecl
has its own documentation, too. In the first case, posted in the question, the doc was attached to GenDecl
, since the AST sees the individual struct type definitions of "contractions" of the parenthesized-version of type definitions, and wants to handle all definitions the same, whether they are grouped or not. The same thing would happen with variable definitions, as in:
// some general docs
var (
// v docs
v int
// v2 docs
v2 string
)
So if you wish to parse comments with pure AST, you need to be aware that this is how it works. But the preferred method, as @mjibson suggested, is to use go/doc
. Good luck!
答案2
得分: 12
你需要使用go/doc
包从ast中提取文档:
package main
import (
"fmt"
"go/doc"
"go/parser"
"go/token"
)
// FirstType文档
type FirstType struct {
// FirstMember文档
FirstMember string
}
// SecondType文档
type SecondType struct {
// SecondMember文档
SecondMember string
}
// Main文档
func main() {
fset := token.NewFileSet() // 位置相对于fset
d, err := parser.ParseDir(fset, "./", nil, parser.ParseComments)
if err != nil {
fmt.Println(err)
return
}
for k, f := range d {
fmt.Println("package", k)
p := doc.New(f, "./", 0)
for _, t := range p.Types {
fmt.Println(" type", t.Name)
fmt.Println(" docs:", t.Doc)
}
}
}
英文:
You need to use the go/doc
package to extract documentation from the ast:
package main
import (
"fmt"
"go/doc"
"go/parser"
"go/token"
)
// FirstType docs
type FirstType struct {
// FirstMember docs
FirstMember string
}
// SecondType docs
type SecondType struct {
// SecondMember docs
SecondMember string
}
// Main docs
func main() {
fset := token.NewFileSet() // positions are relative to fset
d, err := parser.ParseDir(fset, "./", nil, parser.ParseComments)
if err != nil {
fmt.Println(err)
return
}
for k, f := range d {
fmt.Println("package", k)
p := doc.New(f, "./", 0)
for _, t := range p.Types {
fmt.Println(" type", t.Name)
fmt.Println(" docs:", t.Doc)
}
}
}
答案3
得分: 0
解析所有带有注释// typescript:interface的结构体
func TestStructDoc(t *testing.T) {
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
fmt.Println(path, info.Size())
fset := token.NewFileSet()
d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
for k, f := range d {
fmt.Println("package", k)
p := doc.New(f, "./", 0)
for _, t := range p.Types {
fmt.Println(" type", t.Name)
fmt.Println(" docs:", t.Doc)
if strings.HasPrefix(t.Doc, "typescript:interface") {
for _, spec := range t.Decl.Specs {
switch spec.(type) {
case *ast.TypeSpec:
typeSpec := spec.(*ast.TypeSpec)
fmt.Printf("Struct: name=%s\n", typeSpec.Name.Name)
switch typeSpec.Type.(type) {
case *ast.StructType:
structType := typeSpec.Type.(*ast.StructType)
for _, field := range structType.Fields.List {
i := field.Type.(*ast.Ident)
fieldType := i.Name
for _, name := range field.Names {
fmt.Printf("\tField: name=%s type=%s\n", name.Name, fieldType)
}
}
}
}
}
}
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
希望这可以帮到你!如果有任何其他问题,请随时问。
英文:
parse all structs with comment // typescript:interface
func TestStructDoc(t *testing.T) {
err := filepath.Walk(".",
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
fmt.Println(path, info.Size())
fset := token.NewFileSet()
d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
for k, f := range d {
fmt.Println("package", k)
p := doc.New(f, "./", 0)
for _, t := range p.Types {
fmt.Println(" type", t.Name)
fmt.Println(" docs:", t.Doc)
if strings.HasPrefix(t.Doc, "typescript:interface") {
for _, spec := range t.Decl.Specs {
switch spec.(type) {
case *ast.TypeSpec:
typeSpec := spec.(*ast.TypeSpec)
fmt.Printf("Struct: name=%s\n", typeSpec.Name.Name)
switch typeSpec.Type.(type) {
case *ast.StructType:
structType := typeSpec.Type.(*ast.StructType)
for _, field := range structType.Fields.List {
i := field.Type.(*ast.Ident)
fieldType := i.Name
for _, name := range field.Names {
fmt.Printf("\tField: name=%s type=%s\n", name.Name, fieldType)
}
}
}
}
}
}
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
答案4
得分: 0
这不是你的问题,但对于其他搜索并想知道为什么他们的代码无法正确解析注释的人来说:
请确保记得传递parser.ParseComments
标志!
英文:
Wasn't your problem, but for anyone else searching and wondering why their code isn't parsing comments properly:
Make sure you remember to pass the parser.ParseComments
flag!!!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论