Go解析器无法检测到结构类型的文档注释。

huangapple go评论85阅读模式
英文:

Go parser not detecting Doc comments on struct type

问题

我正在尝试使用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

英文:

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 docsSecondType 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(&quot;%s:\tFuncDecl %s\t%s\n&quot;, fset.Position(n.Pos()), x.Name, x.Doc.Text())
		case *ast.TypeSpec:
			fmt.Printf(&quot;%s:\tTypeSpec %s\t%s\n&quot;, fset.Position(n.Pos()), x.Name, x.Doc.Text())
		case *ast.Field:
			fmt.Printf(&quot;%s:\tField %s\t%s\n&quot;, fset.Position(n.Pos()), x.Names, x.Doc.Text())
		case *ast.GenDecl:
			fmt.Printf(&quot;%s:\tGenDecl %s\n&quot;, fset.Position(n.Pos()), x.Doc.Text())
		}

		return true
	})
}

Running this gives us:

main.go:3:1:	GenDecl %!s(*ast.CommentGroup=&lt;nil&gt;)
main.go:11:1:	GenDecl &amp;{[%!s(*ast.Comment=&amp;{69 // FirstType docs})]}
main.go:11:6:	TypeSpec FirstType	%!s(*ast.CommentGroup=&lt;nil&gt;)
main.go:13:2:	Field [FirstMember]	&amp;{[%!s(*ast.Comment=&amp;{112 // FirstMember docs})]}
main.go:17:1:	GenDecl &amp;{[%!s(*ast.Comment=&amp;{155 // SecondType docs})]}
main.go:17:6:	TypeSpec SecondType	%!s(*ast.CommentGroup=&lt;nil&gt;)
main.go:19:2:	Field [SecondMember]	&amp;{[%!s(*ast.Comment=&amp;{200 // SecondMember docs})]}
main.go:23:1:	FuncDecl main	&amp;{[%!s(*ast.Comment=&amp;{245 // Main docs})]}
main.go:33:23:	Field [n]	%!s(*ast.CommentGroup=&lt;nil&gt;)
main.go:33:35:	Field []	%!s(*ast.CommentGroup=&lt;nil&gt;)

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 := &amp;ast.GenDecl{
   504	 Doc: d.Doc,
   505	 // don&#39;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&#39;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=&lt;nil&gt;)
main.go:11:1:	GenDecl &amp;{[%!s(*ast.Comment=&amp;{69 // This documents FirstType and SecondType together})]}
main.go:13:2:	TypeSpec FirstType	&amp;{[%!s(*ast.Comment=&amp;{129 // FirstType docs})]}
main.go:15:3:	Field [FirstMember]	&amp;{[%!s(*ast.Comment=&amp;{169 // FirstMember docs})]}
main.go:19:2:	TypeSpec SecondType	&amp;{[%!s(*ast.Comment=&amp;{215 // SecondType docs})]}
main.go:21:3:	Field [SecondMember]	&amp;{[%!s(*ast.Comment=&amp;{257 // SecondMember docs})]}
main.go:26:1:	FuncDecl main	&amp;{[%!s(*ast.Comment=&amp;{306 // Main docs})]}
main.go:36:23:	Field [n]	%!s(*ast.CommentGroup=&lt;nil&gt;)
main.go:36:35:	Field []	%!s(*ast.CommentGroup=&lt;nil&gt;)

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 (
	&quot;fmt&quot;
	&quot;go/doc&quot;
	&quot;go/parser&quot;
	&quot;go/token&quot;
)

// 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, &quot;./&quot;, nil, parser.ParseComments)
	if err != nil {
		fmt.Println(err)
		return
	}

	for k, f := range d {
		fmt.Println(&quot;package&quot;, k)
		p := doc.New(f, &quot;./&quot;, 0)

		for _, t := range p.Types {
			fmt.Println(&quot;  type&quot;, t.Name)
			fmt.Println(&quot;    docs:&quot;, 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(&quot;.&quot;,
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(&quot;package&quot;, k)
p := doc.New(f, &quot;./&quot;, 0)
for _, t := range p.Types {
fmt.Println(&quot;  type&quot;, t.Name)
fmt.Println(&quot;    docs:&quot;, t.Doc)
if strings.HasPrefix(t.Doc, &quot;typescript:interface&quot;) {
for _, spec := range t.Decl.Specs {
switch spec.(type) {
case *ast.TypeSpec:
typeSpec := spec.(*ast.TypeSpec)
fmt.Printf(&quot;Struct: name=%s\n&quot;, 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(&quot;\tField: name=%s type=%s\n&quot;, 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!!!

huangapple
  • 本文由 发表于 2013年10月25日 11:41:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/19580688.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定