Repository: goplus/gop
Pull request #1042 (2021-12-08 08:45:31)

Release v1.0.35
new features:
- gop/x/format: 
  * Replace fmt.Print to builtin
  * Command style first
  * pkg.Fncall starting with lowercase

changes:
- add: link Go+ build assets into Go bin path
- parser: add parser/_testdata/mapfunc
- gop fmt -mvgo: only process .go files
- gox v1.8.1
- golang.org/x/tools v0.1.8



Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..eb4bfe6e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "gomod" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "daily"
diff --git a/.gitignore b/.gitignore
index ab2048b1..227a0dec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,7 @@ coverage.txt
 .gop/
 gop_autogen*.go
 go.json
+format.result
 *.cache
 
 *.exe
diff --git a/cmd/internal/gopfmt/fmt.go b/cmd/internal/gopfmt/fmt.go
index 11b1f150..ed11d826 100644
--- a/cmd/internal/gopfmt/fmt.go
+++ b/cmd/internal/gopfmt/fmt.go
@@ -119,13 +119,13 @@ func walk(path string, d fs.DirEntry, err error) error {
 		}
 	} else {
 		ext := filepath.Ext(path)
-		if _, ok := extGops[ext]; ok {
+		smart := *flagSmart
+		mvgo := smart && *flagMoveGo
+		if _, ok := extGops[ext]; ok && (!mvgo || ext == ".go") {
 			procCnt++
 			if *flagNotExec {
 				fmt.Println("gop fmt", path)
 			} else {
-				smart := *flagSmart
-				mvgo := smart && *flagMoveGo
 				err = gopfmt(path, smart && (mvgo || ext != ".go"), mvgo)
 				if err != nil {
 					report(err)
diff --git a/cmd/make.go b/cmd/make.go
index 74382d52..d761eef7 100644
--- a/cmd/make.go
+++ b/cmd/make.go
@@ -23,6 +23,7 @@ import (
 	"bytes"
 	"flag"
 	"fmt"
+	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -36,7 +37,7 @@ func checkPathExist(path string, isDir bool) bool {
 	if isDir {
 		return isExists && stat.IsDir()
 	}
-	return isExists
+	return isExists && !stat.IsDir()
 }
 
 // Path returns single path to check
@@ -72,6 +73,7 @@ func getGopRoot() string {
 var gopRoot = getGopRoot()
 var initCommandExecuteEnv = os.Environ()
 var commandExecuteEnv = initCommandExecuteEnv
+var gopBinFiles = []string{"gop", "gopfmt"}
 
 func execCommand(command string, arg ...string) (string, string, error) {
 	var stdout, stderr bytes.Buffer
@@ -132,6 +134,62 @@ func detectGopBinPath() string {
 	return filepath.Join(gopRoot, "bin")
 }
 
+func detectGoBinPath() string {
+	goBin, ok := os.LookupEnv("GOBIN")
+	if ok {
+		return goBin
+	}
+
+	goPath, ok := os.LookupEnv("GOPATH")
+	if ok {
+		list := filepath.SplitList(goPath)
+		if len(list) > 0 {
+			// Put in first directory of $GOPATH.
+			return filepath.Join(list[0], "bin")
+		}
+	}
+
+	homeDir, _ := os.UserHomeDir()
+	return filepath.Join(homeDir, "go", "bin")
+}
+
+func linkGoplusToLocalBin() string {
+	println("Start Linking.")
+
+	gopBinPath := detectGopBinPath()
+	goBinPath := detectGoBinPath()
+	if !checkPathExist(gopBinPath, true) {
+		log.Fatalf("Error: %s is not existed, you should build Go+ before linking.\n", gopBinPath)
+	}
+	if !checkPathExist(goBinPath, true) {
+		if err := os.MkdirAll(goBinPath, 0755); err != nil {
+			fmt.Printf("Error: target directory %s is not existed and we can't create one.\n", goBinPath)
+			log.Fatalln(err)
+		}
+	}
+
+	for _, file := range gopBinFiles {
+		sourceFile := filepath.Join(gopBinPath, file)
+		if !checkPathExist(sourceFile, false) {
+			log.Fatalf("Error: %s is not existed, you should build Go+ before linking.\n", sourceFile)
+		}
+		targetLink := filepath.Join(goBinPath, file)
+		if checkPathExist(targetLink, false) {
+			// Delete existed one
+			if err := os.Remove(targetLink); err != nil {
+				log.Fatalln(err)
+			}
+		}
+		if err := os.Symlink(sourceFile, targetLink); err != nil {
+			log.Fatalln(err)
+		}
+		fmt.Printf("Link %s to %s successfully.\n", sourceFile, targetLink)
+	}
+
+	println("End linking.")
+	return goBinPath
+}
+
 func buildGoplusTools(useGoProxy bool) {
 	commandsDir := filepath.Join(gopRoot, "cmd")
 	buildFlags := getGopBuildFlags()
@@ -145,30 +203,32 @@ func buildGoplusTools(useGoProxy bool) {
 
 	// Install Go+ binary files under current ./bin directory.
 	gopBinPath := detectGopBinPath()
-	clean()
-	if err := os.Mkdir(gopBinPath, 0755); err != nil {
-		println(err.Error())
+	if err := os.Mkdir(gopBinPath, 0755); err != nil && !os.IsExist(err) {
 		println("Error: Go+ can't create ./bin directory to put build assets.")
-		os.Exit(1)
+		log.Fatalln(err)
 	}
 
-	println("Installing Go+ tools...")
+	println("Installing Go+ tools...\n")
 	os.Chdir(commandsDir)
 	buildOutput, buildErr, err := execCommand("go", "build", "-o", gopBinPath, "-v", "-ldflags", buildFlags, "./...")
-	println(buildErr)
+	print(buildErr)
 	if err != nil {
-		println(err.Error())
-		os.Exit(1)
+		log.Fatalln(err)
 	}
-	println(buildOutput)
+	print(buildOutput)
+
+	installPath := linkGoplusToLocalBin()
 
-	println("Go+ tools installed successfully!")
-	showHelpPostInstall()
+	println("\nGo+ tools installed successfully!")
+
+	if _, _, err := execCommand("gop", "version"); err != nil {
+		showHelpPostInstall(installPath)
+	}
 }
 
-func showHelpPostInstall() {
+func showHelpPostInstall(installPath string) {
 	println("\nNEXT STEP:")
-	println("\nWe just installed Go+ into the directory: ", detectGopBinPath())
+	println("\nWe just installed Go+ into the directory: ", installPath)
 	message := `
 To setup a better Go+ development environment,
 we recommend you add the above install directory into your PATH environment variable.
@@ -199,9 +259,22 @@ func runTestcases() {
 
 func clean() {
 	gopBinPath := detectGopBinPath()
+	goBinPath := detectGoBinPath()
+
+	// Clean links
+	for _, file := range gopBinFiles {
+		targetLink := filepath.Join(goBinPath, file)
+		if checkPathExist(targetLink, false) {
+			if err := os.Remove(targetLink); err != nil {
+				log.Fatalln(err)
+			}
+		}
+	}
+
+	// Clean build binary files
 	if checkPathExist(gopBinPath, true) {
 		if err := os.RemoveAll(gopBinPath); err != nil {
-			println(err.Error())
+			log.Fatalln(err)
 		}
 	}
 }
diff --git a/format/format.go b/format/format.go
index 3ee26f05..cae090ae 100644
--- a/format/format.go
+++ b/format/format.go
@@ -28,7 +28,6 @@ package format
 
 import (
 	"bytes"
-	"fmt"
 	"io"
 
 	"github.com/goplus/gop/ast"
@@ -77,8 +76,10 @@ func Node(dst io.Writer, fset *token.FileSet, node interface{}) error {
 		}
 		file, err = parser.ParseFile(fset, "", buf.Bytes(), parserMode)
 		if err != nil {
+			_, err = dst.Write(buf.Bytes())
+			return err
 			// We should never get here. If we do, provide good diagnostic.
-			return fmt.Errorf("format.Node internal error (%s)", err)
+			// return fmt.Errorf("format.Node internal error (%s)", err)
 		}
 		ast.SortImports(fset, file)
 
diff --git a/go.mod b/go.mod
index a9214761..1b56ba14 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/goplus/gop
 go 1.16
 
 require (
-	github.com/goplus/gox v1.8.0
+	github.com/goplus/gox v1.8.1
 	github.com/qiniu/x v1.11.5
 	golang.org/x/mod v0.5.1
-	golang.org/x/tools v0.1.7
+	golang.org/x/tools v0.1.8
 )
diff --git a/go.sum b/go.sum
index 5d556051..d0e06736 100644
--- a/go.sum
+++ b/go.sum
@@ -1,31 +1,31 @@
-github.com/goplus/gox v1.8.0 h1:54OTOoFpAqxU55ZI4EBs8EzDzGw0yzPHSOxgr+U5iT0=
-github.com/goplus/gox v1.8.0/go.mod h1:fUMG5YrEvPjSfGqPnP1gfrZvWlVuA1/9lZy3s/5+Aes=
+github.com/goplus/gox v1.8.1 h1:nMn6J3Jv6zNHr6Ul2GkUqhLCm6m0tWt8jGpI15iSaEw=
+github.com/goplus/gox v1.8.1/go.mod h1:XWqV75HImeysI8+Q9gAdYyvwlP8jds24xsgYRwTO3JI=
 github.com/qiniu/x v1.11.5 h1:TYr5cl4g2yoHAZeDK4MTjKF6CMoG+IHlCDvvM5qym6U=
 github.com/qiniu/x v1.11.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
-github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
-golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
+golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
diff --git a/parser/parser.go b/parser/parser.go
index e1ecaf4c..fab81edf 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1214,7 +1214,9 @@ func (p *parser) parseMapType() *ast.MapType {
 	key := p.parseType()
 	p.expect(token.RBRACK)
 	value := p.parseType()
-
+	if debugParseOutput {
+		log.Printf("ast.MapType{Key: %v, Value: %v}\n", key, value)
+	}
 	return &ast.MapType{Map: pos, Key: key, Value: value}
 }
 
@@ -1378,7 +1380,7 @@ func (p *parser) isCommand(x ast.Expr) bool {
 // types of the form [...]T. Callers must verify the result.
 // If lhs is set and the result is an identifier, it is not resolved.
 //
-func (p *parser) parseOperand(lhs, allowTuple bool) ast.Expr {
+func (p *parser) parseOperand(lhs, allowTuple, allowCmd bool) ast.Expr {
 	if p.trace {
 		defer un(trace(p, "Operand"))
 	}
@@ -1435,8 +1437,17 @@ func (p *parser) parseOperand(lhs, allowTuple bool) ast.Expr {
 			return p.parseLiteralValueOrMapComprehension()
 		}
 
+	case token.MAP:
+		oldpos, oldlit := p.pos, p.lit // Go+: save token to allow map() as a function
+		p.next()
+		pos, tok := p.pos, p.tok
+		p.unget(oldpos, token.MAP, oldlit)
+		if tok == token.LBRACK && (!allowCmd || oldpos+3 == pos) {
+			break
+		}
+		fallthrough
 	case token.GOTO, token.BREAK, token.CONTINUE, token.FALLTHROUGH:
-		// token.RANGE, token.IMPORT, token.MAP, token.TYPE, token.SELECT, token.INTERFACE
+		// token.RANGE, token.IMPORT, token.TYPE, token.SELECT, token.INTERFACE:
 		// Go+: allow goto() as a function
 		p.tok = token.IDENT
 		x := p.parseIdent()
@@ -1844,7 +1855,7 @@ func (p *parser) parsePrimaryExpr(lhs, allowTuple, allowCmd bool) ast.Expr {
 		defer un(trace(p, "PrimaryExpr"))
 	}
 
-	x := p.parseOperand(lhs, allowTuple)
+	x := p.parseOperand(lhs, allowTuple, allowCmd)
 L:
 	for {
 		switch p.tok {
@@ -2889,10 +2900,13 @@ func (p *parser) parseStmt(allowCmd bool) (s ast.Stmt) {
 		s = &ast.DeclStmt{Decl: p.parseGenDecl(p.tok, p.parseValueSpec)}
 	case
 		// tokens that may start an expression
-		token.IDENT, token.INT, token.FLOAT, token.IMAG, token.RAT, token.CHAR, token.STRING, token.FUNC, token.LPAREN, // operands
-		token.LBRACK, token.STRUCT, token.MAP, token.CHAN, token.INTERFACE, // composite types
-		token.ADD, token.SUB, token.MUL, token.AND, token.XOR, token.ARROW, token.NOT: // unary operators
-		s, _ = p.parseSimpleStmt(labelOk, allowCmd && p.tok == token.IDENT)
+		token.INT, token.FLOAT, token.IMAG, token.RAT, token.CHAR, token.STRING, token.FUNC, token.LPAREN, // operands
+		token.ADD, token.SUB, token.MUL, token.AND, token.XOR, token.ARROW, token.NOT, // unary operators
+		token.LBRACK, token.STRUCT, token.CHAN, token.INTERFACE: // composite types
+		allowCmd = false
+		fallthrough
+	case token.IDENT, token.MAP: // operands
+		s, _ = p.parseSimpleStmt(labelOk, allowCmd)
 		// because of the required look-ahead, labeled statements are
 		// parsed by parseSimpleStmt - don't expect a semicolon after
 		// them
diff --git a/x/format/README.md b/x/format/README.md
index 8b76f1bb..1b308cee 100644
--- a/x/format/README.md
+++ b/x/format/README.md
@@ -40,7 +40,7 @@ a := 0
 ```
 
 
-### Replace fmt.Print to builtin (NotImpl)
+### Replace fmt.Print to builtin
 
 ```go
 import "fmt"
@@ -62,7 +62,7 @@ Note:
 * ...
 
 
-### Command style first (NotImpl)
+### Command style first
 
 ```go
 import "fmt"
@@ -83,7 +83,7 @@ Note:
 * Only the outermost function call statement is converted into command style. So `fmt.Println(fmt.Println("Hello world"))` is converted into `println println("Hello world")`, not `println println "Hello world"`.
 
 
-### Function call starting with lowercase (NotImpl)
+### pkg.Fncall starting with lowercase
 
 ```go
 import "math"
diff --git a/x/format/format.go b/x/format/format.go
new file mode 100644
index 00000000..112489ef
--- /dev/null
+++ b/x/format/format.go
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package format
+
+import (
+	"go/token"
+
+	"github.com/goplus/gop/ast"
+)
+
+// -----------------------------------------------------------------------------
+
+var printFuncs = map[string]string{
+	"Errorf":   "errorf",
+	"Print":    "print",
+	"Printf":   "printf",
+	"Println":  "println",
+	"Fprint":   "fprint",
+	"Fprintf":  "fprintf",
+	"Fprintln": "fprintln",
+	"Sprint":   "sprint",
+	"Sprintf":  "sprintf",
+	"Sprintln": "sprintln",
+}
+
+func fmtToBuiltin(ctx *importCtx, sel *ast.Ident, ref *ast.Expr) bool {
+	if ctx.pkgPath == "fmt" {
+		if v, ok := printFuncs[sel.Name]; ok {
+			*ref = &ast.Ident{NamePos: sel.NamePos, Name: v}
+			return true
+		}
+	}
+	return false
+}
+
+func commandStyleFirst(v *ast.CallExpr) {
+	switch v.Fun.(type) {
+	case *ast.Ident, *ast.SelectorExpr:
+		if v.NoParenEnd == token.NoPos {
+			v.NoParenEnd = v.Rparen
+		}
+	}
+}
+
+func fncallStartingLowerCase(v *ast.CallExpr) {
+	switch fn := v.Fun.(type) {
+	case *ast.SelectorExpr:
+		startWithLowerCase(fn.Sel)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/format/gopstyle.go b/x/format/gopstyle.go
index 66828b29..9afe25ee 100644
--- a/x/format/gopstyle.go
+++ b/x/format/gopstyle.go
@@ -1,7 +1,25 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package format
 
 import (
 	"bytes"
+	"path"
+	"strconv"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/format"
@@ -47,10 +65,7 @@ func Gopstyle(file *ast.File) {
 			*/
 		}
 	}
-}
-
-func identEqual(v *ast.Ident, name string) bool {
-	return v != nil && v.Name == name
+	formatFile(file)
 }
 
 func findFuncDecl(decls []ast.Decl, name string) int {
@@ -64,4 +79,131 @@ func findFuncDecl(decls []ast.Decl, name string) int {
 	return -1
 }
 
+func findDecl(decls []ast.Decl, v ast.Decl) int {
+	for i, decl := range decls {
+		if decl == v {
+			return i
+		}
+	}
+	return -1
+}
+
+func findSpec(specs []ast.Spec, v ast.Spec) int {
+	for i, spec := range specs {
+		if spec == v {
+			return i
+		}
+	}
+	return -1
+}
+
+func deleteDecl(decls []ast.Decl, v ast.Decl) []ast.Decl {
+	if idx := findDecl(decls, v); idx >= 0 {
+		decls = append(decls[:idx], decls[idx+1:]...)
+	}
+	return decls
+}
+
+func deleteSpec(specs []ast.Spec, v ast.Spec) []ast.Spec {
+	if idx := findSpec(specs, v); idx >= 0 {
+		specs = append(specs[:idx], specs[idx+1:]...)
+	}
+	return specs
+}
+
+func startWithLowerCase(v *ast.Ident) {
+	if c := v.Name[0]; c >= 'A' && c <= 'Z' {
+		v.Name = string(c+('a'-'A')) + v.Name[1:]
+	}
+}
+
+func identEqual(v *ast.Ident, name string) bool {
+	return v != nil && v.Name == name
+}
+
+func toString(l *ast.BasicLit) string {
+	if l.Kind == token.STRING {
+		s, err := strconv.Unquote(l.Value)
+		if err == nil {
+			return s
+		}
+	}
+	panic("TODO: toString - convert ast.BasicLit to string failed")
+}
+
+// -----------------------------------------------------------------------------
+
+type importCtx struct {
+	pkgPath string
+	decl    *ast.GenDecl
+	spec    *ast.ImportSpec
+	isUsed  bool
+}
+
+type formatCtx struct {
+	imports map[string]*importCtx
+}
+
+func formatFile(file *ast.File) {
+	ctx := &formatCtx{
+		imports: make(map[string]*importCtx),
+	}
+	for _, decl := range file.Decls {
+		switch v := decl.(type) {
+		case *ast.FuncDecl:
+			formatFuncDecl(ctx, v)
+		case *ast.GenDecl:
+			switch v.Tok {
+			case token.IMPORT:
+				for _, item := range v.Specs {
+					var spec = item.(*ast.ImportSpec)
+					var pkgPath = toString(spec.Path)
+					var name string
+					if spec.Name == nil {
+						name = path.Base(pkgPath) // TODO: open pkgPath to get pkgName
+					} else {
+						name = spec.Name.Name
+						if name == "." || name == "_" {
+							continue
+						}
+					}
+					ctx.imports[name] = &importCtx{pkgPath: pkgPath, decl: v, spec: spec}
+				}
+			default:
+				formatGenDecl(ctx, v)
+			}
+		}
+	}
+	for _, imp := range ctx.imports {
+		if imp.pkgPath == "fmt" && !imp.isUsed {
+			if len(imp.decl.Specs) == 1 {
+				file.Decls = deleteDecl(file.Decls, imp.decl)
+			} else {
+				imp.decl.Specs = deleteSpec(imp.decl.Specs, imp.spec)
+			}
+		}
+	}
+}
+
+func formatGenDecl(ctx *formatCtx, v *ast.GenDecl) {
+	switch v.Tok {
+	case token.VAR, token.CONST:
+		for _, item := range v.Specs {
+			spec := item.(*ast.ValueSpec)
+			formatType(ctx, spec.Type, &spec.Type)
+			formatExprs(ctx, spec.Values)
+		}
+	case token.TYPE:
+		for _, item := range v.Specs {
+			spec := item.(*ast.TypeSpec)
+			formatType(ctx, spec.Type, &spec.Type)
+		}
+	}
+}
+
+func formatFuncDecl(ctx *formatCtx, v *ast.FuncDecl) {
+	formatFuncType(ctx, v.Type)
+	formatBlockStmt(ctx, v.Body)
+}
+
 // -----------------------------------------------------------------------------
diff --git a/x/format/stmt_expr_or_type.go b/x/format/stmt_expr_or_type.go
new file mode 100644
index 00000000..60bec773
--- /dev/null
+++ b/x/format/stmt_expr_or_type.go
@@ -0,0 +1,299 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package format
+
+import (
+	"log"
+	"reflect"
+
+	"github.com/goplus/gop/ast"
+)
+
+// -----------------------------------------------------------------------------
+
+func formatType(ctx *formatCtx, typ ast.Expr, ref *ast.Expr) {
+	switch t := typ.(type) {
+	case *ast.Ident, nil:
+	case *ast.SelectorExpr:
+		formatSelectorExpr(ctx, t, ref)
+	case *ast.StarExpr:
+		formatType(ctx, t.X, &t.X)
+	case *ast.MapType:
+		formatType(ctx, t.Key, &t.Key)
+		formatType(ctx, t.Value, &t.Value)
+	case *ast.StructType:
+		formatFields(ctx, t.Fields)
+	case *ast.ArrayType:
+		formatExpr(ctx, t.Len, &t.Len)
+		formatType(ctx, t.Elt, &t.Elt)
+	case *ast.ChanType:
+		formatType(ctx, t.Value, &t.Value)
+	case *ast.InterfaceType:
+		formatFields(ctx, t.Methods)
+	case *ast.FuncType:
+		formatFuncType(ctx, t)
+	case *ast.Ellipsis:
+		formatType(ctx, t.Elt, &t.Elt)
+	default:
+		log.Panicln("TODO: format -", reflect.TypeOf(typ))
+	}
+}
+
+func formatFuncType(ctx *formatCtx, t *ast.FuncType) {
+	formatFields(ctx, t.Params)
+	formatFields(ctx, t.Results)
+}
+
+func formatFields(ctx *formatCtx, flds *ast.FieldList) {
+	if flds != nil {
+		for _, fld := range flds.List {
+			formatField(ctx, fld)
+		}
+	}
+}
+
+func formatField(ctx *formatCtx, fld *ast.Field) {
+	formatType(ctx, fld.Type, &fld.Type)
+}
+
+// -----------------------------------------------------------------------------
+
+func formatExprs(ctx *formatCtx, exprs []ast.Expr) {
+	for i, expr := range exprs {
+		formatExpr(ctx, expr, &exprs[i])
+	}
+}
+
+func formatExpr(ctx *formatCtx, expr ast.Expr, ref *ast.Expr) {
+	switch v := expr.(type) {
+	case *ast.Ident, *ast.BasicLit, *ast.BadExpr, nil:
+	case *ast.BinaryExpr:
+		formatExpr(ctx, v.X, &v.X)
+		formatExpr(ctx, v.Y, &v.Y)
+	case *ast.UnaryExpr:
+		formatExpr(ctx, v.X, &v.X)
+	case *ast.CallExpr:
+		formatCallExpr(ctx, v)
+	case *ast.SelectorExpr:
+		formatSelectorExpr(ctx, v, ref)
+	case *ast.SliceExpr:
+		formatSliceExpr(ctx, v)
+	case *ast.IndexExpr:
+		formatExpr(ctx, v.X, &v.X)
+		formatExpr(ctx, v.Index, &v.Index)
+	case *ast.SliceLit:
+		formatExprs(ctx, v.Elts)
+	case *ast.CompositeLit:
+		formatType(ctx, v.Type, &v.Type)
+		formatExprs(ctx, v.Elts)
+	case *ast.StarExpr:
+		formatExpr(ctx, v.X, &v.X)
+	case *ast.KeyValueExpr:
+		formatExpr(ctx, v.Key, &v.Key)
+		formatExpr(ctx, v.Value, &v.Value)
+	case *ast.FuncLit:
+		formatFuncType(ctx, v.Type)
+		formatBlockStmt(ctx, v.Body)
+	case *ast.TypeAssertExpr:
+		formatExpr(ctx, v.X, &v.X)
+		formatType(ctx, v.Type, &v.Type)
+	case *ast.LambdaExpr:
+		formatExprs(ctx, v.Rhs)
+	case *ast.LambdaExpr2:
+		formatBlockStmt(ctx, v.Body)
+	case *ast.RangeExpr:
+		formatRangeExpr(ctx, v)
+	case *ast.ComprehensionExpr:
+		formatComprehensionExpr(ctx, v)
+	case *ast.ErrWrapExpr:
+		formatExpr(ctx, v.X, &v.X)
+		formatExpr(ctx, v.Default, &v.Default)
+	case *ast.ParenExpr:
+		formatExpr(ctx, v.X, &v.X)
+	case *ast.Ellipsis:
+		formatExpr(ctx, v.Elt, &v.Elt)
+	default:
+		formatType(ctx, expr, ref)
+	}
+}
+
+func formatRangeExpr(ctx *formatCtx, v *ast.RangeExpr) {
+	formatExpr(ctx, v.First, &v.First)
+	formatExpr(ctx, v.Last, &v.Last)
+	formatExpr(ctx, v.Expr3, &v.Expr3)
+}
+
+func formatComprehensionExpr(ctx *formatCtx, v *ast.ComprehensionExpr) {
+	formatForPhrases(ctx, v.Fors)
+	formatExpr(ctx, v.Elt, &v.Elt)
+}
+
+func formatForPhrases(ctx *formatCtx, fors []*ast.ForPhrase) {
+	for _, f := range fors {
+		formatForPhrase(ctx, f)
+	}
+}
+
+func formatForPhrase(ctx *formatCtx, v *ast.ForPhrase) {
+	formatExpr(ctx, v.X, &v.X)
+	formatStmt(ctx, v.Init)
+	formatExpr(ctx, v.Cond, &v.Cond)
+}
+
+func formatSliceExpr(ctx *formatCtx, v *ast.SliceExpr) {
+	formatExpr(ctx, v.X, &v.X)
+	formatExpr(ctx, v.Low, &v.Low)
+	formatExpr(ctx, v.High, &v.High)
+	formatExpr(ctx, v.Max, &v.Max)
+}
+
+func formatCallExpr(ctx *formatCtx, v *ast.CallExpr) {
+	formatExpr(ctx, v.Fun, &v.Fun)
+	fncallStartingLowerCase(v)
+	formatExprs(ctx, v.Args)
+}
+
+func formatSelectorExpr(ctx *formatCtx, v *ast.SelectorExpr, ref *ast.Expr) {
+	switch x := v.X.(type) {
+	case *ast.Ident:
+		// TODO: maybe have bugs: `x.Name` may be a variable, not a import.
+		if imp, ok := ctx.imports[x.Name]; ok {
+			if !fmtToBuiltin(imp, v.Sel, ref) {
+				imp.isUsed = true
+			}
+		}
+	default:
+		formatExpr(ctx, x, &v.X)
+	}
+}
+
+// -----------------------------------------------------------------------------
+
+func formatBlockStmt(ctx *formatCtx, stmt *ast.BlockStmt) {
+	if stmt != nil {
+		formatStmts(ctx, stmt.List)
+	}
+}
+
+func formatStmts(ctx *formatCtx, stmts []ast.Stmt) {
+	for _, stmt := range stmts {
+		formatStmt(ctx, stmt)
+	}
+}
+
+func formatStmt(ctx *formatCtx, stmt ast.Stmt) {
+	switch v := stmt.(type) {
+	case *ast.ExprStmt:
+		formatExprStmt(ctx, v)
+	case *ast.AssignStmt:
+		formatAssignStmt(ctx, v)
+	case *ast.IncDecStmt:
+		formatExpr(ctx, v.X, &v.X)
+	case *ast.ForStmt:
+		formatForStmt(ctx, v)
+	case *ast.RangeStmt:
+		formatRangeStmt(ctx, v)
+	case *ast.ForPhraseStmt:
+		formatForPhrase(ctx, v.ForPhrase)
+		formatBlockStmt(ctx, v.Body)
+	case *ast.IfStmt:
+		formatIfStmt(ctx, v)
+	case *ast.CaseClause:
+		formatExprs(ctx, v.List)
+		formatStmts(ctx, v.Body)
+	case *ast.SwitchStmt:
+		formatSwitchStmt(ctx, v)
+	case *ast.TypeSwitchStmt:
+		formatTypeSwitchStmt(ctx, v)
+	case *ast.CommClause:
+		formatStmt(ctx, v.Comm)
+		formatStmts(ctx, v.Body)
+	case *ast.SelectStmt:
+		formatBlockStmt(ctx, v.Body)
+	case *ast.DeclStmt:
+		formatDeclStmt(ctx, v)
+	case *ast.ReturnStmt:
+		formatExprs(ctx, v.Results)
+	case *ast.BlockStmt:
+		formatBlockStmt(ctx, v)
+	case *ast.DeferStmt:
+		formatCallExpr(ctx, v.Call)
+	case *ast.GoStmt:
+		formatCallExpr(ctx, v.Call)
+	case *ast.SendStmt:
+		formatExpr(ctx, v.Chan, &v.Chan)
+		formatExpr(ctx, v.Value, &v.Value)
+	case *ast.LabeledStmt:
+		formatStmt(ctx, v.Stmt)
+	case *ast.BranchStmt, *ast.EmptyStmt, nil, *ast.BadStmt:
+	default:
+		log.Panicln("TODO: formatStmt -", reflect.TypeOf(stmt))
+	}
+}
+
+func formatExprStmt(ctx *formatCtx, v *ast.ExprStmt) {
+	switch x := v.X.(type) {
+	case *ast.CallExpr:
+		commandStyleFirst(x)
+	}
+	formatExpr(ctx, v.X, &v.X)
+}
+
+func formatAssignStmt(ctx *formatCtx, v *ast.AssignStmt) {
+	formatExprs(ctx, v.Lhs)
+	formatExprs(ctx, v.Rhs)
+}
+
+func formatSwitchStmt(ctx *formatCtx, v *ast.SwitchStmt) {
+	formatStmt(ctx, v.Init)
+	formatExpr(ctx, v.Tag, &v.Tag)
+	formatBlockStmt(ctx, v.Body)
+}
+
+func formatTypeSwitchStmt(ctx *formatCtx, v *ast.TypeSwitchStmt) {
+	formatStmt(ctx, v.Init)
+	formatStmt(ctx, v.Assign)
+	formatBlockStmt(ctx, v.Body)
+}
+
+func formatIfStmt(ctx *formatCtx, v *ast.IfStmt) {
+	formatStmt(ctx, v.Init)
+	formatExpr(ctx, v.Cond, &v.Cond)
+	formatBlockStmt(ctx, v.Body)
+	formatStmt(ctx, v.Else)
+}
+
+func formatRangeStmt(ctx *formatCtx, v *ast.RangeStmt) {
+	formatExpr(ctx, v.Key, &v.Key)
+	formatExpr(ctx, v.Value, &v.Value)
+	formatExpr(ctx, v.X, &v.X)
+	formatBlockStmt(ctx, v.Body)
+}
+
+func formatForStmt(ctx *formatCtx, v *ast.ForStmt) {
+	formatStmt(ctx, v.Init)
+	formatExpr(ctx, v.Cond, &v.Cond)
+	formatBlockStmt(ctx, v.Body)
+}
+
+func formatDeclStmt(ctx *formatCtx, v *ast.DeclStmt) {
+	if decl, ok := v.Decl.(*ast.GenDecl); ok {
+		formatGenDecl(ctx, decl)
+	}
+}
+
+// -----------------------------------------------------------------------------


Test patch:
diff --git a/format/_format_test.go b/format/_format_test.go
deleted file mode 100644
index 36d5c4fb..00000000
--- a/format/_format_test.go
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
- * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package format
-
-import (
-	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"testing"
-
-	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/token"
-)
-
-const testfile = "format_test.go"
-
-func diff(t *testing.T, dst, src []byte) {
-	line := 1
-	offs := 0 // line offset
-	for i := 0; i < len(dst) && i < len(src); i++ {
-		d := dst[i]
-		s := src[i]
-		if d != s {
-			t.Errorf("dst:%d: %s\n", line, dst[offs:i+5])
-			t.Errorf("src:%d: %s\n", line, src[offs:i+5])
-			return
-		}
-		if s == '\n' {
-			line++
-			offs = i + 1
-		}
-	}
-	if len(dst) != len(src) {
-		t.Errorf("len(dst) = %d, len(src) = %d\nsrc = %q", len(dst), len(src), src)
-	}
-}
-
-func TestNode(t *testing.T) {
-	src, err := ioutil.ReadFile(testfile)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	fset := token.NewFileSet()
-	file, err := parser.ParseFile(fset, testfile, src, parser.ParseComments)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var buf bytes.Buffer
-
-	if err = Node(&buf, fset, file); err != nil {
-		t.Fatal("Node failed:", err)
-	}
-
-	diff(t, buf.Bytes(), src)
-}
-
-func TestSource(t *testing.T) {
-	src, err := ioutil.ReadFile(testfile)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-
-	diff(t, res, src)
-}
-
-var (
-	gop_main_pkg = `package main
-
-import (
-	"strconv"
-)
-
-func add(x, y string) (int, error) {
-	return strconv.Atoi(x)? + strconv.Atoi(y)?, nil
-}
-
-func addSafe(x, y string) int {
-	return strconv.Atoi(x)?:0 + strconv.Atoi(y)?:0
-}
-
-func test() {
-	println(add("100", "23")!)
-
-	sum, err := add("10", "abc")
-	println(sum, err)
-
-	println(addSafe("10", "abc"))
-}
-
-func main() {
-	println("Hello, Go+")
-	println(1r << 129)
-	println(1/3r + 2/7r*2)
-
-	arr := [1, 3, 5, 7, 11, 13, 17, 19]
-	println(arr)
-
-	e := [[a, b] for a <- arr, a < b for b <- arr, b > 2]
-
-	x := {x: i for i, x <- [1, 3, 5, 7, 11]}
-	y := {x: i for i, x <- [1, 3, 5, 7, 11], i%2 == 1}
-	z := {v: k for k, v <- {1: "Hello", 3: "Hi", 5: "xsw", 7: "Go+"}, k > 3}
-
-	m := {"Hi": 1, "Go+": 2}
-	println(m)
-	println({v: k for k, v <- m})
-	println([k for k, _ <- m])
-	println([v for v <- m])
-
-	sum := 0
-	for x <- [1, 3, 5, 7, 11], x > 3 {
-		sum += x
-	}
-	println("sum(5,7,11):", sum)
-}
-`
-	gop_nopkg = `println("Hello, Go+")
-println(1r << 129)
-println(1/3r + 2/7r*2)
-
-arr := [1, 3, 5, 7, 11, 13, 17, 19]
-println(arr)
-println([x*x for x <- arr, x > 3])
-
-m := {"Hi": 1, "Go+": 2}
-println(m)
-println({v: k for k, v <- m})
-println([k for k, _ <- m])
-println([v for v <- m])
-`
-	gop_nopkg_import = `
-import (
-	"fmt"
-	"strings"
-	"strconv"
-)
-
-func add(x, y string) (int, error) {
-	return strconv.Atoi(x)? + strconv.Atoi(y)?, nil
-}
-
-x := strings.NewReplacer("?", "!").Replace("hello, world???")
-fmt.Println("x:", x)
-sum := 0
-for x <- [1, 3, 5, 7, 11], x > 3 {
-	sum += x
-}
-println("sum(5,7,11):", sum)
-`
-	gop_depth = `
-x := [1, 3.4] // []float64
-println("x:", x)
-
-z := {v+t: k+i for k, v <- {1: "Hello", 3: "Hi", 5: "xsw", 7: "Go+"}, k > 3 for i, t <- ["a"]}
-println(z)
-
-y := 1 + 2*4/5r
-
-a := [1, 3, 5, 7, 11]
-b := [x*x for x <- a, x > 3]
-println(b) // output: [25 49 121]
-
-mapData := {"Hi": 1, "Hello": 2, "Go+": 3}
-reversedMap := {v: k for k, v <- mapData}
-println(reversedMap) // output: map[1:Hi 2:Hello 3:Go+]
-`
-
-	gop_list_map = `
-a := [x*x for x <- [1, 3, 5, 7, 11]]
-b := [x*x for x <- [1, 3, 5, 7, 11], x > 3]
-c := [i+v for i, v <- [1, 3, 5, 7, 11], i%2 == 1]
-d := [k+","+s for k, s <- {"Hello": "xsw", "Hi": "Go+"}]
-
-arr := [1, 2, 3, 4, 5, 6]
-e := [[a, b] for a <- arr, a < b for b <- arr, b > 2]
-
-x := {x: i for i, x <- [1, 3, 5, 7, 11]}
-y := {x: i for i, x <- [1, 3, 5, 7, 11], i%2 == 1}
-z := {v: k for k, v <- {1: "Hello", 3: "Hi", 5: "xsw", 7: "Go+"}, k > 3}
-`
-	gop_err_handle = `
-import (
-	"strconv"
-)
-
-func add(x, y string) (int, error) {
-	return strconv.Atoi(x)? + strconv.Atoi(y)?, nil
-}
-
-func addSafe(x, y string) int {
-	return strconv.Atoi(x)?:0 + strconv.Atoi(y)?:0
-}
-
-println("add", add("100", "23")!)
-
-sum, err := add("10", "abc")
-println("add", sum, err)
-
-println("addSafe", addSafe("10", "abc"))
-`
-)
-
-func TestGopSourceMain(t *testing.T) {
-	src := []byte(gop_main_pkg)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-func TestGopSourceNoMain(t *testing.T) {
-	src := []byte(gop_nopkg)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-func TestGopSourceImportsNoMain(t *testing.T) {
-	src := []byte(gop_nopkg_import)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-func TestGopSourceDepth(t *testing.T) {
-	src := []byte(gop_depth)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-func TestGopSourceListMap(t *testing.T) {
-	src := []byte(gop_list_map)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-func TestGopSourceErrorHandle(t *testing.T) {
-	src := []byte(gop_err_handle)
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-	diff(t, res, src)
-}
-
-// Test cases that are expected to fail are marked by the prefix "ERROR".
-// The formatted result must look the same as the input for successful tests.
-var tests = []string{
-	// declaration lists
-	`import "go/format"`,
-	"var x int",
-	"var x int\n\ntype T struct{}",
-
-	// statement lists
-	"x := 0",
-	"f(a, b, c)\nvar x int = f(1, 2, 3)",
-
-	// indentation, leading and trailing space
-	"\tx := 0\n\tgo f()",
-	"\tx := 0\n\tgo f()\n\n\n",
-	"\n\t\t\n\n\tx := 0\n\tgo f()\n\n\n",
-	"\n\t\t\n\n\t\t\tx := 0\n\t\t\tgo f()\n\n\n",
-	"\n\t\t\n\n\t\t\tx := 0\n\t\t\tconst s = `\nfoo\n`\n\n\n",     // no indentation added inside raw strings
-	"\n\t\t\n\n\t\t\tx := 0\n\t\t\tconst s = `\n\t\tfoo\n`\n\n\n", // no indentation removed inside raw strings
-
-	// comments
-	"/* Comment */",
-	"\t/* Comment */ ",
-	"\n/* Comment */ ",
-	"i := 5 /* Comment */",         // issue #5551
-	"\ta()\n//line :1",             // issue #11276
-	"\t//xxx\n\ta()\n//line :2",    // issue #11276
-	"\ta() //line :1\n\tb()\n",     // issue #11276
-	"x := 0\n//line :1\n//line :2", // issue #11276
-
-	// whitespace
-	"",     // issue #11275
-	" ",    // issue #11275
-	"\t",   // issue #11275
-	"\t\t", // issue #11275
-	"\n",   // issue #11275
-	"\n\n", // issue #11275
-	"\t\n", // issue #11275
-
-	// erroneous programs
-	"ERROR1 + 2 +",
-	"ERRORx :=  0",
-}
-
-func String(s string) (string, error) {
-	res, err := Source([]byte(s))
-	if err != nil {
-		return "", err
-	}
-	return string(res), nil
-}
-
-func TestPartial(t *testing.T) {
-	for _, src := range tests {
-		if strings.HasPrefix(src, "ERROR") {
-			// test expected to fail
-			src = src[5:] // remove ERROR prefix
-			res, err := String(src)
-			if err == nil && res == src {
-				t.Errorf("formatting succeeded but was expected to fail:\n%q", src)
-			}
-		} else {
-			// test expected to succeed
-			res, err := String(src)
-			if err != nil {
-				t.Errorf("formatting failed (%s):\n%q", err, src)
-			} else if strings.TrimSpace(res) != src {
-				t.Errorf("formatting incorrect:\nsource: %q\nresult: %q", src, res)
-			}
-		}
-		break
-	}
-}
-
-func testFrom(t *testing.T, fpath string) {
-	src, err := ioutil.ReadFile(fpath)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	res, err := Source(src)
-	if err != nil {
-		t.Fatal("Source failed:", err)
-	}
-
-	diff(t, res, src)
-}
-
-func TestFromTestdata(t *testing.T) {
-	dir, err := os.Getwd()
-	if err != nil {
-		t.Fatal("Getwd failed:", err)
-	}
-	dir = filepath.Join(dir, "../parser/_testdata")
-	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if !info.IsDir() && filepath.Ext(path) == ".gop" {
-			t.Log(path)
-			testFrom(t, path)
-		}
-		return nil
-	})
-}
diff --git a/parser/_testdata/collection/collection.gop b/parser/_testdata/collection/collection.gop
new file mode 100644
index 00000000..730644c5
--- /dev/null
+++ b/parser/_testdata/collection/collection.gop
@@ -0,0 +1,113 @@
+// We often need our programs to perform operations on
+// collections of data, like selecting all items that
+// satisfy a given predicate or mapping all items to a new
+// collection with a custom function.
+
+// In some languages it's idiomatic to use [generic](http://en.wikipedia.org/wiki/Generic_programming)
+// data structures and algorithms. Go does not support
+// generics; in Go it's common to provide collection
+// functions if and when they are specifically needed for
+// your program and data types.
+
+// Here are some example collection functions for slices
+// of `strings`. You can use these examples to build your
+// own functions. Note that in some cases it may be
+// clearest to just inline the collection-manipulating
+// code directly, instead of creating and calling a
+// helper function.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+// Index returns the first index of the target string `t`, or
+// -1 if no match is found.
+func Index(vs []string, t string) int {
+	for i, v := range vs {
+		if v == t {
+			return i
+		}
+	}
+	return -1
+}
+
+// Include returns `true` if the target string t is in the
+// slice.
+func Include(vs []string, t string) bool {
+	return Index(vs, t) >= 0
+}
+
+// Any returns `true` if one of the strings in the slice
+// satisfies the predicate `f`.
+func Any(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if f(v) {
+			return true
+		}
+	}
+	return false
+}
+
+// All returns `true` if all of the strings in the slice
+// satisfy the predicate `f`.
+func All(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if !f(v) {
+			return false
+		}
+	}
+	return true
+}
+
+// Filter returns a new slice containing all strings in the
+// slice that satisfy the predicate `f`.
+func Filter(vs []string, f func(string) bool) []string {
+	vsf := make([]string, 0)
+	for _, v := range vs {
+		if f(v) {
+			vsf = append(vsf, v)
+		}
+	}
+	return vsf
+}
+
+// Map returns a new slice containing the results of applying
+// the function `f` to each string in the original slice.
+func Map(vs []string, f func(string) string) []string {
+	vsm := make([]string, len(vs))
+	for i, v := range vs {
+		vsm[i] = f(v)
+	}
+	return vsm
+}
+
+func main() {
+
+	// Here we try out our various collection functions.
+	var strs = []string{"peach", "apple", "pear", "plum"}
+
+	fmt.Println(Index(strs, "pear"))
+
+	fmt.Println(Include(strs, "grape"))
+
+	fmt.Println(Any(strs, func(v string) bool {
+		return strings.HasPrefix(v, "p")
+	}))
+
+	fmt.Println(All(strs, func(v string) bool {
+		return strings.HasPrefix(v, "p")
+	}))
+
+	fmt.Println(Filter(strs, func(v string) bool {
+		return strings.Contains(v, "e")
+	}))
+
+	// The above examples all used anonymous functions,
+	// but you can also use named functions of the correct
+	// type.
+	fmt.Println(Map(strs, strings.ToUpper))
+
+}
diff --git a/parser/_testdata/collection/parser.expect b/parser/_testdata/collection/parser.expect
new file mode 100644
index 00000000..cbe329c5
--- /dev/null
+++ b/parser/_testdata/collection/parser.expect
@@ -0,0 +1,865 @@
+package main
+
+file collection.gop
+ast.GenDecl:
+  Tok: import
+  Specs:
+    ast.ImportSpec:
+      Path:
+        ast.BasicLit:
+          Kind: STRING
+          Value: "fmt"
+    ast.ImportSpec:
+      Path:
+        ast.BasicLit:
+          Kind: STRING
+          Value: "strings"
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // Index returns the first index of the target string `t`, or
+        ast.Comment:
+          Text: // -1 if no match is found.
+  Name:
+    ast.Ident:
+      Name: Index
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: t
+              Type:
+                ast.Ident:
+                  Name: string
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.Ident:
+                  Name: int
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.RangeStmt:
+          Key:
+            ast.Ident:
+              Name: i
+          Value:
+            ast.Ident:
+              Name: v
+          Tok: :=
+          X:
+            ast.Ident:
+              Name: vs
+          Body:
+            ast.BlockStmt:
+              List:
+                ast.IfStmt:
+                  Cond:
+                    ast.BinaryExpr:
+                      X:
+                        ast.Ident:
+                          Name: v
+                      Op: ==
+                      Y:
+                        ast.Ident:
+                          Name: t
+                  Body:
+                    ast.BlockStmt:
+                      List:
+                        ast.ReturnStmt:
+                          Results:
+                            ast.Ident:
+                              Name: i
+        ast.ReturnStmt:
+          Results:
+            ast.UnaryExpr:
+              Op: -
+              X:
+                ast.BasicLit:
+                  Kind: INT
+                  Value: 1
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // Include returns `true` if the target string t is in the
+        ast.Comment:
+          Text: // slice.
+  Name:
+    ast.Ident:
+      Name: Include
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: t
+              Type:
+                ast.Ident:
+                  Name: string
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.Ident:
+                  Name: bool
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.ReturnStmt:
+          Results:
+            ast.BinaryExpr:
+              X:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Index
+                  Args:
+                    ast.Ident:
+                      Name: vs
+                    ast.Ident:
+                      Name: t
+              Op: >=
+              Y:
+                ast.BasicLit:
+                  Kind: INT
+                  Value: 0
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // Any returns `true` if one of the strings in the slice
+        ast.Comment:
+          Text: // satisfies the predicate `f`.
+  Name:
+    ast.Ident:
+      Name: Any
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: f
+              Type:
+                ast.FuncType:
+                  Params:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: string
+                  Results:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: bool
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.Ident:
+                  Name: bool
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.RangeStmt:
+          Key:
+            ast.Ident:
+              Name: _
+          Value:
+            ast.Ident:
+              Name: v
+          Tok: :=
+          X:
+            ast.Ident:
+              Name: vs
+          Body:
+            ast.BlockStmt:
+              List:
+                ast.IfStmt:
+                  Cond:
+                    ast.CallExpr:
+                      Fun:
+                        ast.Ident:
+                          Name: f
+                      Args:
+                        ast.Ident:
+                          Name: v
+                  Body:
+                    ast.BlockStmt:
+                      List:
+                        ast.ReturnStmt:
+                          Results:
+                            ast.Ident:
+                              Name: true
+        ast.ReturnStmt:
+          Results:
+            ast.Ident:
+              Name: false
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // All returns `true` if all of the strings in the slice
+        ast.Comment:
+          Text: // satisfy the predicate `f`.
+  Name:
+    ast.Ident:
+      Name: All
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: f
+              Type:
+                ast.FuncType:
+                  Params:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: string
+                  Results:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: bool
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.Ident:
+                  Name: bool
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.RangeStmt:
+          Key:
+            ast.Ident:
+              Name: _
+          Value:
+            ast.Ident:
+              Name: v
+          Tok: :=
+          X:
+            ast.Ident:
+              Name: vs
+          Body:
+            ast.BlockStmt:
+              List:
+                ast.IfStmt:
+                  Cond:
+                    ast.UnaryExpr:
+                      Op: !
+                      X:
+                        ast.CallExpr:
+                          Fun:
+                            ast.Ident:
+                              Name: f
+                          Args:
+                            ast.Ident:
+                              Name: v
+                  Body:
+                    ast.BlockStmt:
+                      List:
+                        ast.ReturnStmt:
+                          Results:
+                            ast.Ident:
+                              Name: false
+        ast.ReturnStmt:
+          Results:
+            ast.Ident:
+              Name: true
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // Filter returns a new slice containing all strings in the
+        ast.Comment:
+          Text: // slice that satisfy the predicate `f`.
+  Name:
+    ast.Ident:
+      Name: Filter
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: f
+              Type:
+                ast.FuncType:
+                  Params:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: string
+                  Results:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: bool
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.AssignStmt:
+          Lhs:
+            ast.Ident:
+              Name: vsf
+          Tok: :=
+          Rhs:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: make
+              Args:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+                ast.BasicLit:
+                  Kind: INT
+                  Value: 0
+        ast.RangeStmt:
+          Key:
+            ast.Ident:
+              Name: _
+          Value:
+            ast.Ident:
+              Name: v
+          Tok: :=
+          X:
+            ast.Ident:
+              Name: vs
+          Body:
+            ast.BlockStmt:
+              List:
+                ast.IfStmt:
+                  Cond:
+                    ast.CallExpr:
+                      Fun:
+                        ast.Ident:
+                          Name: f
+                      Args:
+                        ast.Ident:
+                          Name: v
+                  Body:
+                    ast.BlockStmt:
+                      List:
+                        ast.AssignStmt:
+                          Lhs:
+                            ast.Ident:
+                              Name: vsf
+                          Tok: =
+                          Rhs:
+                            ast.CallExpr:
+                              Fun:
+                                ast.Ident:
+                                  Name: append
+                              Args:
+                                ast.Ident:
+                                  Name: vsf
+                                ast.Ident:
+                                  Name: v
+        ast.ReturnStmt:
+          Results:
+            ast.Ident:
+              Name: vsf
+ast.FuncDecl:
+  Doc:
+    ast.CommentGroup:
+      List:
+        ast.Comment:
+          Text: // Map returns a new slice containing the results of applying
+        ast.Comment:
+          Text: // the function `f` to each string in the original slice.
+  Name:
+    ast.Ident:
+      Name: Map
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: vs
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+            ast.Field:
+              Names:
+                ast.Ident:
+                  Name: f
+              Type:
+                ast.FuncType:
+                  Params:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: string
+                  Results:
+                    ast.FieldList:
+                      List:
+                        ast.Field:
+                          Type:
+                            ast.Ident:
+                              Name: string
+      Results:
+        ast.FieldList:
+          List:
+            ast.Field:
+              Type:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.AssignStmt:
+          Lhs:
+            ast.Ident:
+              Name: vsm
+          Tok: :=
+          Rhs:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: make
+              Args:
+                ast.ArrayType:
+                  Elt:
+                    ast.Ident:
+                      Name: string
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: len
+                  Args:
+                    ast.Ident:
+                      Name: vs
+        ast.RangeStmt:
+          Key:
+            ast.Ident:
+              Name: i
+          Value:
+            ast.Ident:
+              Name: v
+          Tok: :=
+          X:
+            ast.Ident:
+              Name: vs
+          Body:
+            ast.BlockStmt:
+              List:
+                ast.AssignStmt:
+                  Lhs:
+                    ast.IndexExpr:
+                      X:
+                        ast.Ident:
+                          Name: vsm
+                      Index:
+                        ast.Ident:
+                          Name: i
+                  Tok: =
+                  Rhs:
+                    ast.CallExpr:
+                      Fun:
+                        ast.Ident:
+                          Name: f
+                      Args:
+                        ast.Ident:
+                          Name: v
+        ast.ReturnStmt:
+          Results:
+            ast.Ident:
+              Name: vsm
+ast.FuncDecl:
+  Name:
+    ast.Ident:
+      Name: main
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.DeclStmt:
+          Decl:
+            ast.GenDecl:
+              Doc:
+                ast.CommentGroup:
+                  List:
+                    ast.Comment:
+                      Text: // Here we try out our various collection functions.
+              Tok: var
+              Specs:
+                ast.ValueSpec:
+                  Names:
+                    ast.Ident:
+                      Name: strs
+                  Values:
+                    ast.CompositeLit:
+                      Type:
+                        ast.ArrayType:
+                          Elt:
+                            ast.Ident:
+                              Name: string
+                      Elts:
+                        ast.BasicLit:
+                          Kind: STRING
+                          Value: "peach"
+                        ast.BasicLit:
+                          Kind: STRING
+                          Value: "apple"
+                        ast.BasicLit:
+                          Kind: STRING
+                          Value: "pear"
+                        ast.BasicLit:
+                          Kind: STRING
+                          Value: "plum"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Index
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.BasicLit:
+                      Kind: STRING
+                      Value: "pear"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Include
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.BasicLit:
+                      Kind: STRING
+                      Value: "grape"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Any
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.FuncLit:
+                      Type:
+                        ast.FuncType:
+                          Params:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Names:
+                                    ast.Ident:
+                                      Name: v
+                                  Type:
+                                    ast.Ident:
+                                      Name: string
+                          Results:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Type:
+                                    ast.Ident:
+                                      Name: bool
+                      Body:
+                        ast.BlockStmt:
+                          List:
+                            ast.ReturnStmt:
+                              Results:
+                                ast.CallExpr:
+                                  Fun:
+                                    ast.SelectorExpr:
+                                      X:
+                                        ast.Ident:
+                                          Name: strings
+                                      Sel:
+                                        ast.Ident:
+                                          Name: HasPrefix
+                                  Args:
+                                    ast.Ident:
+                                      Name: v
+                                    ast.BasicLit:
+                                      Kind: STRING
+                                      Value: "p"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: All
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.FuncLit:
+                      Type:
+                        ast.FuncType:
+                          Params:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Names:
+                                    ast.Ident:
+                                      Name: v
+                                  Type:
+                                    ast.Ident:
+                                      Name: string
+                          Results:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Type:
+                                    ast.Ident:
+                                      Name: bool
+                      Body:
+                        ast.BlockStmt:
+                          List:
+                            ast.ReturnStmt:
+                              Results:
+                                ast.CallExpr:
+                                  Fun:
+                                    ast.SelectorExpr:
+                                      X:
+                                        ast.Ident:
+                                          Name: strings
+                                      Sel:
+                                        ast.Ident:
+                                          Name: HasPrefix
+                                  Args:
+                                    ast.Ident:
+                                      Name: v
+                                    ast.BasicLit:
+                                      Kind: STRING
+                                      Value: "p"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Filter
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.FuncLit:
+                      Type:
+                        ast.FuncType:
+                          Params:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Names:
+                                    ast.Ident:
+                                      Name: v
+                                  Type:
+                                    ast.Ident:
+                                      Name: string
+                          Results:
+                            ast.FieldList:
+                              List:
+                                ast.Field:
+                                  Type:
+                                    ast.Ident:
+                                      Name: bool
+                      Body:
+                        ast.BlockStmt:
+                          List:
+                            ast.ReturnStmt:
+                              Results:
+                                ast.CallExpr:
+                                  Fun:
+                                    ast.SelectorExpr:
+                                      X:
+                                        ast.Ident:
+                                          Name: strings
+                                      Sel:
+                                        ast.Ident:
+                                          Name: Contains
+                                  Args:
+                                    ast.Ident:
+                                      Name: v
+                                    ast.BasicLit:
+                                      Kind: STRING
+                                      Value: "e"
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.SelectorExpr:
+                  X:
+                    ast.Ident:
+                      Name: fmt
+                  Sel:
+                    ast.Ident:
+                      Name: Println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: Map
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.SelectorExpr:
+                      X:
+                        ast.Ident:
+                          Name: strings
+                      Sel:
+                        ast.Ident:
+                          Name: ToUpper
diff --git a/parser/_testdata/mapfunc/map.gop b/parser/_testdata/mapfunc/map.gop
new file mode 100644
index 00000000..69d9b258
--- /dev/null
+++ b/parser/_testdata/mapfunc/map.gop
@@ -0,0 +1,4 @@
+map strs, toUpper
+map ["hello", "world"], toUpper
+map[string]int{"Hi": 1}
+println map(strs, strings.ToUpper)
diff --git a/parser/_testdata/mapfunc/parser.expect b/parser/_testdata/mapfunc/parser.expect
new file mode 100644
index 00000000..f31c99f1
--- /dev/null
+++ b/parser/_testdata/mapfunc/parser.expect
@@ -0,0 +1,85 @@
+package main
+
+file map.gop
+noEntrypoint
+ast.FuncDecl:
+  Name:
+    ast.Ident:
+      Name: main
+  Type:
+    ast.FuncType:
+      Params:
+        ast.FieldList:
+  Body:
+    ast.BlockStmt:
+      List:
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: map
+              Args:
+                ast.Ident:
+                  Name: strs
+                ast.Ident:
+                  Name: toUpper
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: map
+              Args:
+                ast.SliceLit:
+                  Elts:
+                    ast.BasicLit:
+                      Kind: STRING
+                      Value: "hello"
+                    ast.BasicLit:
+                      Kind: STRING
+                      Value: "world"
+                ast.Ident:
+                  Name: toUpper
+        ast.ExprStmt:
+          X:
+            ast.CompositeLit:
+              Type:
+                ast.MapType:
+                  Key:
+                    ast.Ident:
+                      Name: string
+                  Value:
+                    ast.Ident:
+                      Name: int
+              Elts:
+                ast.KeyValueExpr:
+                  Key:
+                    ast.BasicLit:
+                      Kind: STRING
+                      Value: "Hi"
+                  Value:
+                    ast.BasicLit:
+                      Kind: INT
+                      Value: 1
+        ast.ExprStmt:
+          X:
+            ast.CallExpr:
+              Fun:
+                ast.Ident:
+                  Name: println
+              Args:
+                ast.CallExpr:
+                  Fun:
+                    ast.Ident:
+                      Name: map
+                  Args:
+                    ast.Ident:
+                      Name: strs
+                    ast.SelectorExpr:
+                      X:
+                        ast.Ident:
+                          Name: strings
+                      Sel:
+                        ast.Ident:
+                          Name: ToUpper
diff --git a/printer/gop_test.go b/printer/gop_test.go
index 25517a3c..cfd198dd 100644
--- a/printer/gop_test.go
+++ b/printer/gop_test.go
@@ -21,10 +21,12 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/format"
+	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/printer"
 	"github.com/goplus/gop/token"
 )
@@ -102,21 +104,49 @@ func diffBytes(t *testing.T, dst, src []byte) {
 	}
 }
 
-func testFrom(t *testing.T, fpath string) {
+const (
+	excludeFormatSource = 1
+	excludeFormatNode   = 2
+)
+
+func testFrom(t *testing.T, fpath, sel string, mode int) {
+	if sel != "" && !strings.Contains(fpath, sel) {
+		return
+	}
 	src, err := ioutil.ReadFile(fpath)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	res, err := format.Source(src, fpath)
-	if err != nil {
-		t.Fatal("Source failed:", err)
+	if (mode & excludeFormatSource) == 0 {
+		t.Run("format.Source "+fpath, func(t *testing.T) {
+			res, err := format.Source(src, fpath)
+			if err != nil {
+				t.Fatal("Source failed:", err)
+			}
+			diffBytes(t, res, src)
+		})
 	}
 
-	diffBytes(t, res, src)
+	if (mode & excludeFormatNode) == 0 {
+		t.Run("format.Node "+fpath, func(t *testing.T) {
+			fset := token.NewFileSet()
+			f, err := parser.ParseFile(fset, fpath, src, parser.ParseComments)
+			if err != nil {
+				t.Fatal(err)
+			}
+			var buf bytes.Buffer
+			err = format.Node(&buf, fset, f)
+			if err != nil {
+				t.Fatal(err)
+			}
+			diffBytes(t, buf.Bytes(), src)
+		})
+	}
 }
 
 func TestFromTestdata(t *testing.T) {
+	sel := ""
 	dir, err := os.Getwd()
 	if err != nil {
 		t.Fatal("Getwd failed:", err)
@@ -127,14 +157,14 @@ func TestFromTestdata(t *testing.T) {
 			return err
 		}
 		if !info.IsDir() && filepath.Ext(path) == ".gop" {
-			t.Log(path)
-			testFrom(t, path)
+			testFrom(t, path, sel, 0)
 		}
 		return nil
 	})
 }
 
 func TestFromParse(t *testing.T) {
+	sel := ""
 	dir, err := os.Getwd()
 	if err != nil {
 		t.Fatal("Getwd failed:", err)
@@ -149,8 +179,7 @@ func TestFromParse(t *testing.T) {
 			return nil
 		}
 		if !info.IsDir() && filepath.Ext(name) == ".gop" {
-			t.Log(path)
-			testFrom(t, path)
+			testFrom(t, path, sel, 0)
 		}
 		return nil
 	})
diff --git a/x/format/_testdata/collection/format.expect b/x/format/_testdata/collection/format.expect
new file mode 100644
index 00000000..d4f69322
--- /dev/null
+++ b/x/format/_testdata/collection/format.expect
@@ -0,0 +1,106 @@
+// We often need our programs to perform operations on
+// collections of data, like selecting all items that
+// satisfy a given predicate or mapping all items to a new
+// collection with a custom function.
+
+// In some languages it's idiomatic to use [generic](http://en.wikipedia.org/wiki/Generic_programming)
+// data structures and algorithms. Go does not support
+// generics; in Go it's common to provide collection
+// functions if and when they are specifically needed for
+// your program and data types.
+
+// Here are some example collection functions for slices
+// of `strings`. You can use these examples to build your
+// own functions. Note that in some cases it may be
+// clearest to just inline the collection-manipulating
+// code directly, instead of creating and calling a
+// helper function.
+
+import (
+	"strings"
+)
+
+// Index returns the first index of the target string `t`, or
+// -1 if no match is found.
+func Index(vs []string, t string) int {
+	for i, v := range vs {
+		if v == t {
+			return i
+		}
+	}
+	return -1
+}
+
+// Include returns `true` if the target string t is in the
+// slice.
+func Include(vs []string, t string) bool {
+	return Index(vs, t) >= 0
+}
+
+// Any returns `true` if one of the strings in the slice
+// satisfies the predicate `f`.
+func Any(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if f(v) {
+			return true
+		}
+	}
+	return false
+}
+
+// All returns `true` if all of the strings in the slice
+// satisfy the predicate `f`.
+func All(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if !f(v) {
+			return false
+		}
+	}
+	return true
+}
+
+// Filter returns a new slice containing all strings in the
+// slice that satisfy the predicate `f`.
+func Filter(vs []string, f func(string) bool) []string {
+	vsf := make([]string, 0)
+	for _, v := range vs {
+		if f(v) {
+			vsf = append(vsf, v)
+		}
+	}
+	return vsf
+}
+
+// Map returns a new slice containing the results of applying
+// the function `f` to each string in the original slice.
+func Map(vs []string, f func(string) string) []string {
+	vsm := make([]string, len(vs))
+	for i, v := range vs {
+		vsm[i] = f(v)
+	}
+	return vsm
+}
+
+// Here we try out our various collection functions.
+var strs = []string{"peach", "apple", "pear", "plum"}
+
+println Index(strs, "pear")
+
+println Include(strs, "grape")
+
+println Any(strs, func(v string) bool {
+	return strings.hasPrefix(v, "p")
+})
+
+println All(strs, func(v string) bool {
+	return strings.hasPrefix(v, "p")
+})
+
+println Filter(strs, func(v string) bool {
+	return strings.contains(v, "e")
+})
+
+// The above examples all used anonymous functions,
+// but you can also use named functions of the correct
+// type.
+println Map(strs, strings.ToUpper)
diff --git a/x/format/_testdata/collection/index.gop b/x/format/_testdata/collection/index.gop
new file mode 100644
index 00000000..730644c5
--- /dev/null
+++ b/x/format/_testdata/collection/index.gop
@@ -0,0 +1,113 @@
+// We often need our programs to perform operations on
+// collections of data, like selecting all items that
+// satisfy a given predicate or mapping all items to a new
+// collection with a custom function.
+
+// In some languages it's idiomatic to use [generic](http://en.wikipedia.org/wiki/Generic_programming)
+// data structures and algorithms. Go does not support
+// generics; in Go it's common to provide collection
+// functions if and when they are specifically needed for
+// your program and data types.
+
+// Here are some example collection functions for slices
+// of `strings`. You can use these examples to build your
+// own functions. Note that in some cases it may be
+// clearest to just inline the collection-manipulating
+// code directly, instead of creating and calling a
+// helper function.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+)
+
+// Index returns the first index of the target string `t`, or
+// -1 if no match is found.
+func Index(vs []string, t string) int {
+	for i, v := range vs {
+		if v == t {
+			return i
+		}
+	}
+	return -1
+}
+
+// Include returns `true` if the target string t is in the
+// slice.
+func Include(vs []string, t string) bool {
+	return Index(vs, t) >= 0
+}
+
+// Any returns `true` if one of the strings in the slice
+// satisfies the predicate `f`.
+func Any(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if f(v) {
+			return true
+		}
+	}
+	return false
+}
+
+// All returns `true` if all of the strings in the slice
+// satisfy the predicate `f`.
+func All(vs []string, f func(string) bool) bool {
+	for _, v := range vs {
+		if !f(v) {
+			return false
+		}
+	}
+	return true
+}
+
+// Filter returns a new slice containing all strings in the
+// slice that satisfy the predicate `f`.
+func Filter(vs []string, f func(string) bool) []string {
+	vsf := make([]string, 0)
+	for _, v := range vs {
+		if f(v) {
+			vsf = append(vsf, v)
+		}
+	}
+	return vsf
+}
+
+// Map returns a new slice containing the results of applying
+// the function `f` to each string in the original slice.
+func Map(vs []string, f func(string) string) []string {
+	vsm := make([]string, len(vs))
+	for i, v := range vs {
+		vsm[i] = f(v)
+	}
+	return vsm
+}
+
+func main() {
+
+	// Here we try out our various collection functions.
+	var strs = []string{"peach", "apple", "pear", "plum"}
+
+	fmt.Println(Index(strs, "pear"))
+
+	fmt.Println(Include(strs, "grape"))
+
+	fmt.Println(Any(strs, func(v string) bool {
+		return strings.HasPrefix(v, "p")
+	}))
+
+	fmt.Println(All(strs, func(v string) bool {
+		return strings.HasPrefix(v, "p")
+	}))
+
+	fmt.Println(Filter(strs, func(v string) bool {
+		return strings.Contains(v, "e")
+	}))
+
+	// The above examples all used anonymous functions,
+	// but you can also use named functions of the correct
+	// type.
+	fmt.Println(Map(strs, strings.ToUpper))
+
+}
diff --git a/x/format/_testdata/gopsyntax/format.expect b/x/format/_testdata/gopsyntax/format.expect
new file mode 100644
index 00000000..b783df28
--- /dev/null
+++ b/x/format/_testdata/gopsyntax/format.expect
@@ -0,0 +1,9 @@
+a := [1, 2]
+plot x => x * x
+onStart x => {
+	println x
+}
+for i <- :10 {
+}
+println [x*x for x <- :10]
+print f()?:10
diff --git a/x/format/_testdata/gopsyntax/index.gop b/x/format/_testdata/gopsyntax/index.gop
new file mode 100644
index 00000000..b783df28
--- /dev/null
+++ b/x/format/_testdata/gopsyntax/index.gop
@@ -0,0 +1,9 @@
+a := [1, 2]
+plot x => x * x
+onStart x => {
+	println x
+}
+for i <- :10 {
+}
+println [x*x for x <- :10]
+print f()?:10
diff --git a/x/format/_testdata/syntax/format.expect b/x/format/_testdata/syntax/format.expect
new file mode 100644
index 00000000..52884fd9
--- /dev/null
+++ b/x/format/_testdata/syntax/format.expect
@@ -0,0 +1,29 @@
+func f(...T) {
+}
+
+a++
+arr := [...]int{1: 3}
+m := make(map[string]chan interface{})
+for range arr[1:] {
+foo:
+	*(p) = 1
+	println f(arr...)
+	m["a"] <- true
+}
+for i := 0; i < 10; i++ {
+	var t *T
+	if false {
+	} else {
+	}
+	select {
+	case <-m["a"]:
+	}
+	switch {
+	case i == 10:
+	}
+	switch t.(type) {
+	case int:
+		defer f()
+		go f().println("Hi")
+	}
+}
diff --git a/x/format/_testdata/syntax/index.gop b/x/format/_testdata/syntax/index.gop
new file mode 100644
index 00000000..27493942
--- /dev/null
+++ b/x/format/_testdata/syntax/index.gop
@@ -0,0 +1,31 @@
+import "fmt"
+
+func f(...T) {
+}
+
+a++
+arr := [...]int{1: 3}
+m := make(map[string]chan interface{})
+for range arr[1:] {
+foo:
+	*(p) = 1
+	fmt.Println(f(arr...))
+	m["a"] <- true
+}
+for i := 0; i < 10; i++ {
+	var t *T
+	if false {
+	} else {
+	}
+	select {
+	case <-m["a"]:
+	}
+	switch {
+	case i == 10:
+	}
+	switch t.(type) {
+	case int:
+		defer f()
+		go f().Println("Hi")
+	}
+}
diff --git a/x/format/gopstyle_test.go b/x/format/gopstyle_test.go
index b0a0f037..5b1e8159 100644
--- a/x/format/gopstyle_test.go
+++ b/x/format/gopstyle_test.go
@@ -1,11 +1,25 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package format
 
 import (
 	"testing"
 )
 
-// -----------------------------------------------------------------------------
-
 func testFormat(t *testing.T, name string, src, expect string) {
 	t.Run(name, func(t *testing.T) {
 		result, err := GopstyleSource([]byte(src), name)
@@ -18,8 +32,10 @@ func testFormat(t *testing.T, name string, src, expect string) {
 	})
 }
 
-func TestBasic1(t *testing.T) {
-	testFormat(t, "hello world", `package main
+// -----------------------------------------------------------------------------
+
+func TestMain(t *testing.T) {
+	testFormat(t, "hello world 1", `package main
 
 import "fmt"
 
@@ -28,17 +44,12 @@ func main() {
 	// say hello
 	fmt.Println("Hello world")
 }
-`, `import "fmt"
-
-// this is main
+`, `// this is main
 
 // say hello
-fmt.Println("Hello world")
+println "Hello world"
 `)
-}
-
-func TestBasic2(t *testing.T) {
-	testFormat(t, "hello world", `package main
+	testFormat(t, "hello world 2", `package main
 
 import "fmt"
 
@@ -50,31 +61,284 @@ func main() {
 
 func f() {
 }
-`, `import "fmt"
-
-// this is main
+`, `// this is main
 func main() {
 	// say hello
-	fmt.Println("Hello world")
+	println "Hello world"
+}
+
+func f() {
+}
+`)
+	testFormat(t, "hello world 3", `package main
+
+import "fmt"
+
+func f() {
+}
+`, `func f() {
+}
+`)
 }
 
+// -----------------------------------------------------------------------------
+
+func TestPrint(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
 func f() {
+	fmt.Print("hello")
+}
+`, `func f() {
+	print "hello"
 }
 `)
 }
 
-func TestBasic3(t *testing.T) {
-	testFormat(t, "hello world", `package main
+func TestPrintf(t *testing.T) {
+	testFormat(t, "print", `package main
 
 import "fmt"
 
 func f() {
+	fmt.Printf("hello")
+}
+`, `func f() {
+	printf "hello"
+}
+`)
+}
+
+func TestPrintln(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
+func f() {
+	fmt.Println("hello")
+}
+`, `func f() {
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnGroup(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import (
+	"fmt"
+)
+
+func f() {
+	fmt.Println("hello")
+}
+`, `func f() {
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtCalls(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import . "errors"
+import "fmt"
+
+func f() {
+	fmt.Errorf("%w", New("hello"))
+	fmt.Println("hello")
+}
+`, `import . "errors"
+
+func f() {
+	errorf "%w", New("hello")
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtCallsGroup(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import (
+	"errors"
+	"fmt"
+)
+
+func f() {
+	fmt.Errorf("%w", errors.New("hello"))
+	fmt.Println("hello")
+}
+`, `import (
+	"errors"
+)
+
+func f() {
+	errorf "%w", errors.new("hello")
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtCallsWithAssign(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "errors"
+import "fmt"
+
+func f() {
+	_ = fmt.Errorf("%w", errors.New("hello"))
+	fmt.Println("hello")
+}
+`, `import "errors"
+
+func f() {
+	_ = errorf("%w", errors.new("hello"))
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtCallsWithGroupWithAssign(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import (
+	"errors"
+	"fmt"
+)
+
+func f() {
+	_ = fmt.Errorf("%w", errors.New("hello"))
+	fmt.Println("hello")
+}
+`, `import (
+	"errors"
+)
+
+func f() {
+	_ = errorf("%w", errors.new("hello"))
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtDecls(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
+func f() {
+	_ = fmt.Stringer
+	fmt.Println("hello")
 }
 `, `import "fmt"
 
 func f() {
+	_ = fmt.Stringer
+	println "hello"
 }
 `)
 }
 
-// -----------------------------------------------------------------------------
+func TestPrintlnWithOtherFmtVars(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
+func f() {
+	var _ fmt.Stringer
+	fmt.Println("hello")
+}
+`, `import "fmt"
+
+func f() {
+	var _ fmt.Stringer
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnWithOtherFmtType(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
+func f() {
+	var _ struct {
+		fmt.Stringer
+		fn func()
+	}
+	fmt.Println("hello")
+}
+`, `import "fmt"
+
+func f() {
+	var _ struct {
+		fmt.Stringer
+		fn func()
+	}
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnImportAlias(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import fmt1 "fmt"
+
+func f() {
+	fmt1.Println("hello")
+}
+`, `func f() {
+	println "hello"
+}
+`)
+}
+
+func TestPrintlnImportMultiAliases(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import (
+	fmt1 "fmt"
+	fmt2 "fmt"
+)
+
+func f() {
+	fmt1.Println(1)
+	fmt2.Println(2)
+}
+`, `func f() {
+	println 1
+	println 2
+}
+`)
+}
+
+func TestPrintlnImportMultiAliasesDifferentGroups(t *testing.T) {
+	testFormat(t, "print", `package main
+
+import "fmt"
+
+import (
+	fmt1 "fmt"
+	fmt2 "fmt"
+)
+
+func f() {
+	var _ fmt.Stringer
+	fmt1.Println(1)
+	fmt2.Println(2)
+}
+`, `import "fmt"
+
+func f() {
+	var _ fmt.Stringer
+	println 1
+	println 2
+}
+`)
+}
diff --git a/x/format/gopstyledir_test.go b/x/format/gopstyledir_test.go
new file mode 100644
index 00000000..b87e9c03
--- /dev/null
+++ b/x/format/gopstyledir_test.go
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package format
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"strings"
+	"testing"
+)
+
+// -----------------------------------------------------------------------------
+
+func TestFromTestdata(t *testing.T) {
+	sel := ""
+	dir, err := os.Getwd()
+	if err != nil {
+		t.Fatal("Getwd failed:", err)
+	}
+	dir = path.Join(dir, "./_testdata")
+	fis, err := ioutil.ReadDir(dir)
+	if err != nil {
+		t.Fatal("ReadDir failed:", err)
+	}
+	for _, fi := range fis {
+		name := fi.Name()
+		if strings.HasPrefix(name, "_") {
+			continue
+		}
+		t.Run(name, func(t *testing.T) {
+			testFrom(t, dir+"/"+name, sel)
+		})
+	}
+}
+
+func testFrom(t *testing.T, pkgDir, sel string) {
+	if sel != "" && !strings.Contains(pkgDir, sel) {
+		return
+	}
+	log.Println("Formatting", pkgDir)
+	file := pkgDir + "/index.gop"
+	src, err := os.ReadFile(file)
+	if err != nil {
+		t.Fatal(err)
+	}
+	ret, err := GopstyleSource(src, file)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expect, err := os.ReadFile(pkgDir + "/format.expect")
+	if err != nil {
+		t.Fatal(err)
+	}
+	diffBytes(t, pkgDir+"/format.result", ret, expect)
+}
+
+func diffBytes(t *testing.T, outfile string, dst, src []byte) {
+	line := 1
+	offs := 0 // line offset
+	for i := 0; i < len(dst) && i < len(src); i++ {
+		d := dst[i]
+		s := src[i]
+		if d != s {
+			os.WriteFile(outfile, dst, 0666)
+			t.Errorf("dst:%d: %s\n", line, dst[offs:])
+			t.Errorf("src:%d: %s\n", line, src[offs:])
+			return
+		}
+		if s == '\n' {
+			line++
+			offs = i + 1
+		}
+	}
+	if len(dst) != len(src) {
+		t.Errorf("len(dst) = %d, len(src) = %d\ndst = %q\nsrc = %q", len(dst), len(src), dst, src)
+	}
+}
+
+// -----------------------------------------------------------------------------