70

GitHub - dave/dst: Decorated Syntax Tree - manipulate Go source with perfect fid...

 5 years ago
source link: https://github.com/dave/dst
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

README.md

Build Status Documentation stability-experimental 68747470733a2f2f696d672e736869656c64732e696f2f62616467652f70617472656f6e2d646f6e6174652d79656c6c6f772e737667

Decorated Syntax Tree

The dst package enables manipulation of a Go syntax tree with high fidelity. Decorations (e.g. comments and line spacing) remain attached to the correct nodes as the tree is modified.

Where does go/ast break?

The go/ast package wasn't created with source manipulation as an intended use-case. Comments are stored by their byte offset instead of attached to nodes. Because of this, re-arranging nodes breaks the output. See this golang issue for more information.

Consider this example where we want to reverse the order of the two statements. As you can see the comments don't remain attached to the correct nodes:

code := `package a

func main(){
	var a int    // foo
	var b string // bar
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "a.go", code, parser.ParseComments)
if err != nil {
	panic(err)
}

list := f.Decls[0].(*ast.FuncDecl).Body.List
list[0], list[1] = list[1], list[0]

if err := format.Node(os.Stdout, fset, f); err != nil {
	panic(err)
}

//Output:
//package a
//
//func main() {
//	// foo
//	var b string
//	var a int
//	// bar
//}

Here's the same example using dst:

code := `package a

func main(){
	var a int    // foo
	var b string // bar
}
`
f, err := decorator.Parse(code)
if err != nil {
	panic(err)
}

list := f.Decls[0].(*dst.FuncDecl).Body.List
list[0], list[1] = list[1], list[0]

if err := decorator.Print(f); err != nil {
	panic(err)
}

//Output:
//package a
//
//func main() {
//	var b string // bar
//	var a int    // foo
//}

Examples

Comments

Comments are added at decoration attachment points. See generated-decorations.go for a full list of these points, along with demonstration code of where they are rendered in the output.

The decoration attachment points have convenience functions Add, Replace, Clear and All to accomplish common tasks. Use the full text of your comment including the // or /**/ markers. When adding a line comment, a newline is automatically rendered.

code := `package main

func main() {
	println("Hello World!")
}`
f, err := decorator.Parse(code)
if err != nil {
	panic(err)
}

call := f.Decls[0].(*dst.FuncDecl).Body.List[0].(*dst.ExprStmt).X.(*dst.CallExpr)

call.Decs.Start.Add("// you can add comments at the start...")
call.Decs.Fun.Add("/* ...in the middle... */")
call.Decs.End.Add("// or at the end.")

if err := decorator.Print(f); err != nil {
	panic(err)
}

//Output:
//package main
//
//func main() {
//	// you can add comments at the start...
//	println /* ...in the middle... */ ("Hello World!") // or at the end.
//}

Line spacing

The Space property marks the node as having a line space (new line or empty line) before the node. These spaces are rendered before any decorations attached to the Start decoration point. The After property is similar but rendered after the node (and after any End decorations).

code := `package main

func main() {
	println(a, b, c)
}`
f, err := decorator.Parse(code)
if err != nil {
	panic(err)
}

call := f.Decls[0].(*dst.FuncDecl).Body.List[0].(*dst.ExprStmt).X.(*dst.CallExpr)

call.Decs.Space = dst.EmptyLine
call.Decs.After = dst.EmptyLine

for _, v := range call.Args {
	v := v.(*dst.Ident)
	v.Decs.Space = dst.NewLine
	v.Decs.After = dst.NewLine
}

if err := decorator.Print(f); err != nil {
	panic(err)
}

//Output:
//package main
//
//func main() {
//
//	println(
//		a,
//		b,
//		c,
//	)
//
//}

Common properties

The common decoration properties (Start, End, Space and After) occur on all Expr, Stmt and Decl nodes, so are available on those interfaces:

code := `package main

func main() {
	var i int
	i++
	println(i)
}`
f, err := decorator.Parse(code)
if err != nil {
	panic(err)
}

list := f.Decls[0].(*dst.FuncDecl).Body.List

list[0].SetSpace(dst.EmptyLine)
list[0].End().Add("// the Decorated interface allows access to the common")
list[1].End().Add("// decoration properties (Space, Start, End and After)")
list[2].End().Add("// for all Expr, Stmt and Decl nodes.")
list[2].SetAfter(dst.EmptyLine)

if err := decorator.Print(f); err != nil {
	panic(err)
}

//Output:
//package main
//
//func main() {
//
//	var i int  // the Decorated interface allows access to the common
//	i++        // decoration properties (Space, Start, End and After)
//	println(i) // for all Expr, Stmt and Decl nodes.
//
//}

Newlines as decorations

The Space and After properties cover the majority of cases, but occasionally a newline needs to be rendered inside a node. Simply add a \n decoration to accomplish this.

Apply function from astutil

The dstutil package is a fork of golang.org/x/tools/go/ast/astutil, and provides the Apply function with similar semantics.

Integrating with go/types

Forking the go/types package to use a dst tree as input is non-trivial because go/types uses position information in several places. A work-around is to convert ast to dst using a Decorator. After conversion, this exposes the DstNodes and AstNodes properties which map between ast.Node and dst.Node. This way the go/types package can be used:

code := `package main

func main() {
	var i int
	i++
	println(i)
}`

// Parse the code to AST
fset := token.NewFileSet()
astFile, err := parser.ParseFile(fset, "a.go", code, parser.ParseComments)
if err != nil {
	panic(err)
}

// Invoke the type checker using AST as input
typesInfo := types.Info{
	Defs:	make(map[*ast.Ident]types.Object),
	Uses:	make(map[*ast.Ident]types.Object),
}
conf := &types.Config{}
if _, err := conf.Check("a", fset, []*ast.File{astFile}, &typesInfo); err != nil {
	panic(err)
}

// Decorate the *ast.File to give us a *dst.File
dec := decorator.New()
f := dec.Decorate(fset, astFile).(*dst.File)

// Find the *dst.Ident for the definition of "i"
dstDef := f.Decls[0].(*dst.FuncDecl).Body.List[0].(*dst.DeclStmt).Decl.(*dst.GenDecl).Specs[0].(*dst.ValueSpec).Names[0]

// Find the *ast.Ident using the AstNodes mapping
astDef := dec.AstNodes[dstDef].(*ast.Ident)

// Find the types.Object corresponding to "i"
obj := typesInfo.Defs[astDef]

// Find all the uses of that object
var uses []*dst.Ident
for id, ob := range typesInfo.Uses {
	if ob != obj {
		continue
	}
	uses = append(uses, dec.DstNodes[id].(*dst.Ident))
}

// Change the name of all uses
dstDef.Name = "foo"
for _, id := range uses {
	id.Name = "foo"
}

// Print the DST
if err := decorator.Print(f); err != nil {
	panic(err)
}

//Output:
//package main
//
//func main() {
//	var foo int
//	foo++
//	println(foo)
//}

If you would like to help create a fully dst compatible version of go/types, feel free to continue my work in the types branch.

Status

This is an experimental package under development, but the API is not expected to change much going forward. Please try it out and give feedback.

Chat?

Feel free to create an issue or chat in the #dst Gophers Slack channel.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK