Go解析器未能检测到结构类型的文档注释

22
我正在尝试使用Go的parserast包阅读结构类型上关联的文档注释。在这个例子中,代码只是将自身用作源。
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

4个回答

23

好问题!

查看go/doc的源代码,我们可以看到它在readType函数中处理了相同的情况。在那里,它说:

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     }
...

特别注意,它需要处理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 docsSecondType docs!但这还不够。为什么文档没有附到TypeSpec上?实际上,go/doc/reader.go文件为解决这个问题做了非常大的努力,如果结构体声明没有相关文档,它会生成一个虚假的GenDecl并将其传递给前面提到的readType函数!

   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  }

但这一切为什么?

想象一下,我们稍微改变了问题中的代码类型定义(这种定义结构体的方式并不常见,但在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。祝好运!


12
你需要使用go/doc包从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)
        }
    }
}

2
虽然 go/doc 是一个解决方案,但这仍然无法回答为什么 go/ast 不会在类型(例如 TypeSpec)上提供注释/文档的问题。 - themihai

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)
}

}


0

虽然这并不是你的问题,但对于其他搜索并想知道为什么他们的代码无法正确解析注释的人请注意:

确保记得传递 parser.ParseComments 标志!!!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接