深入理解 Go 語言中的 Testable Examples
隱藏的ast
和parser
包的介紹
2016 年 10 月 10 日
Golang 的工具鏈實現了名為Testable Examples
的功能。如果對該功能沒有什麼印象的話,我強烈建議首先閱讀“ Testable Examples in Go ”
博文進行了解。通過這篇文章我們將瞭解到該功能的整個解決方案以及如何構建其簡化版本。
讓我們看看Testable Examples
的工作原理:
upper_test.go:
package main import ( "fmt" "strings" ) func ExampleToUpperOK() { fmt.Println(strings.ToUpper("foo")) // Output: FOO } func ExampleToUpperFail() { fmt.Println(strings.ToUpper("bar")) // Output: BAr } > Go test -v === RUNExampleToUpperOK --- PASS: ExampleToUpperOK (0.00s) === RUNExampleToUpperFail --- FAIL: ExampleToUpperFail (0.00s) got: BAR want: BAr FAIL exit status 1 FAILGithub.com/mlowicki/sandbox0.008s
與測試函式一樣的Examples
放在xxx_test.go
檔案中,但字首為Example
而不是Test
。go test
命令使用特殊格式的註釋(Output:something
)並將它們與捕獲的資料進行比較,通常寫入stdout
。其他工具(例如godoc
)使用相同的註釋來豐富自動生成的文件。
問題是go test
或godoc
如何從特殊註釋中提取資料?語言中是否有任何祕密機制使其成為可能?或者也許一切都可以用眾所周知的結構來實現?
事實證明,標準庫提供了與 Go 本身解析原始碼相關的元素(分佈在幾個包中)。這些工具生成抽象語法樹並提供訪問特殊註釋的途徑。
抽象語法樹(AST)
AST 是解析時在原始碼中找到的元素的樹形表示。讓我們考慮一個簡單的表示式:
9 /(2 + 1)
可以使用程式碼段生成AST :
expr, err := parser.ParseExpr("9 / (2 + 1)") if err != nil { log.Fatal(err) } ast.Print(nil, expr)
輸出:
0 *ast.BinaryExpr { 1 . X: *ast.BasicLit { 2 . . ValuePos: 1 3 . . Kind: INT 4 . . Value: "9" 5 . } 6 . OpPos: 3 7 . Op: / 8 . Y: *ast.ParenExpr { 9 . . Lparen: 5 10 . . X: *ast.BinaryExpr { 11 . . . X: *ast.BasicLit { 12 . . . . ValuePos: 6 13 . . . . Kind: INT 14 . . . . Value: "2" 15 . . . } 16 . . . OpPos: 8 17 . . . Op: + 18 . . . Y: *ast.BasicLit { 19 . . . . ValuePos: 10 20 . . . . Kind: INT 21 . . . . Value: "1" 22 . . . } 23 . . } 24 . . Rparen: 11 25 . } 26 }
使用圖表可以簡化輸出,其中樹形結構更明顯:
(operator: /) /\ /\ (integer: 9) (parenthesized expression) | | (operator: +) /\ /\ (integer: 2)(integer: 1)
使用 AST 時,兩個標準包是至關重要的:
通常在詞法分析 期間會刪除註釋。有一個特殊的標誌來儲存註釋並將它們放入 AST - parser.ParseComments :
import ( "fmt" "go/parser" "go/token" "log" ) func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "t.go", nil, parser.ParseComments) if err != nil { log.Fatal(err) } for _, group := range f.Comments { fmt.Printf("Comment group %#v\n", group) for _, comment := range group.List { fmt.Printf("Comment %#v\n", comment) } } }
parser.ParseFile
的第三個引數是傳遞給f.ex
的可選引數 , 型別可以是string
或io.Reader
。由於我使用了磁碟中的檔案,因此設定為nil
。
t.go:
package main import "fmt" // a // b func main() { // c fmt.Println("boom!") }
輸出:
Comment group &ast.CommentGroup{List:[]*ast.Comment{(*ast.Comment)(0x820262220), (*ast.Comment)(0x820262240)}} Comment &ast.Comment{Slash:29, Text:"// a"} Comment &ast.Comment{Slash:34, Text:"// b"} Comment group &ast.CommentGroup{List:[]*ast.Comment{(*ast.Comment)(0x8202622c0)}} Comment &ast.Comment{Slash:55, Text:"// c"}
Comment group
指的是一系列註釋,中間沒有任何元素。在上面的示例中,註釋“ a ”
和“ b ”
屬於同一組。
Pos &Position
原始碼中元素的位置使用Pos
型別記錄(其更詳細的對應點是Position
)。它是一個單一的整數值,它對像line
或column
這樣的資訊進行編碼,但Position struct
將它們儲存在不同的欄位中。在外迴圈新增:
fmt.Printf("Position %#v\n", fset.PositionFor(group.Pos(), true))
程式額外輸出:
Position token.Position{Filename:"t.go", Offset:28, Line:5, Column:1} Position token.Position{Filename:"t.go", Offset:54, Line:9, Column:2}
Fileset
位置相對於解析檔案集計算。每個檔案都分配了不相交的範圍,每個位置都位於其中一個範圍內。在我們的例子中,我們只有一個,但需要整個集合來解碼Pos
:
fset.PositionFor(group.Pos(), true)
樹遍歷
包ast
為深度優先遍歷 AST 提供了方便的功能:
ast.Inspect(f, func(n ast.Node) bool { if n != nil { fmt.Println(n) } return true })
由於我們知道如何提取所有註釋,現在是時候找到所有頂級的ExampleXXX
函數了。
doc.Examples
包doc
提供了完全符合我們需要的功能:
package main import ( "fmt" "go/doc" "go/parser" "go/token" "log" ) func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "e.go", nil, parser.ParseComments) if err != nil { log.Fatal(err) } examples := doc.Examples(f) for _, example := range examples { fmt.Println(example.Name) } }
e.go:
package main import "fmt" func ExampleSuccess() { fmt.Println("foo") // Output: foo } func ExampleFail() { fmt.Println("foo") // Output: bar }
輸出:
Fail Success
doc.Examples
沒有任何魔法技能。它依賴於我們已經看到的內容,主要是構建和遍歷抽象語法樹。讓我們建立類似的東西:
package main import ( "fmt" "go/ast" "go/parser" "go/token" "log" "strings" ) func findExampleOutput(block *ast.BlockStmt, comments []*ast.CommentGroup) (string, bool) { var last *ast.CommentGroup for _, group := range comments { if (block.Pos() < group.Pos()) && (block.End() > group.End()) { last = group } } if last != nil { text := last.Text() marker := "Output: " if strings.HasPrefix(text, marker) { return strings.TrimRight(text[len(marker):], "\n"), true } } return "", false } func isExample(fdecl *ast.FuncDecl) bool { return strings.HasPrefix(fdecl.Name.Name, "Example") } func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "e.go", nil, parser.ParseComments) if err != nil { log.Fatal(err) } for _, decl := range f.Decls { fdecl, ok := decl.(*ast.FuncDecl) if !ok { continue } if isExample(fdecl) { output, found := findExampleOutput(fdecl.Body, f.Comments) if found { fmt.Printf("%s needs output '%s' \n", fdecl.Name.Name, output) } } } }
輸出:
ExampleSuccess needs output ‘foo’ ExampleFail needs output 'bar'
註釋不是AST
樹的常規節點。它們可以通過
ast.File
的Comments
欄位訪問(由f.ex. parser.ParseFile
返回)。此列表中的註釋順序與它們在原始碼中顯示的順序相同。要查詢某些塊內的註釋,我們需要比較上面的findExampleOutput
中的位置:
var last *ast.CommentGroup for _, group := range comments { if (block.Pos() < group.Pos()) && (block.End() > group.End()) { last = group } }
if
語句中的條件檢查comment group
是否屬於塊的範圍。
正如我們所看到的那樣,標準庫在解析時提供了很大的支援。那裡的公共類庫使整個工作非常愉快,並且精心設計的程式碼非常緊湊。
如果你喜歡這個帖子並希望獲得有關新帖子的更新,請關注我。點選下面的❤,幫助他人發現這些資料。