├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── testdata ├── go.mod ├── p1 │ ├── file1.go │ ├── p2 │ │ ├── file1.go │ │ └── file2.go │ ├── p3 │ │ ├── testp │ │ │ └── file1.go │ │ ├── imp1.go │ │ └── imp1_test.go │ ├── testp │ │ └── file1.go │ ├── imp1_test.go │ └── imp1.go ├── two │ ├── file1.go │ └── file2.go ├── exprlist.go ├── longstr.go └── longstmt.go ├── go.mod ├── .gitattributes ├── LICENSE ├── write.go ├── README.md ├── load.go ├── write_test.go ├── go.sum ├── load_test.go ├── subst.go ├── main.go ├── parse.go ├── match_test.go └── match.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mvdan 2 | -------------------------------------------------------------------------------- /testdata/go.mod: -------------------------------------------------------------------------------- 1 | module testdata.tld/util 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /testdata/p1/file1.go: -------------------------------------------------------------------------------- 1 | package p1 2 | 3 | var _ = "file1" 4 | -------------------------------------------------------------------------------- /testdata/p1/p2/file1.go: -------------------------------------------------------------------------------- 1 | package p2 2 | 3 | var _ = "file1" 4 | -------------------------------------------------------------------------------- /testdata/p1/p2/file2.go: -------------------------------------------------------------------------------- 1 | package p2 2 | 3 | var _ = "file2" 4 | -------------------------------------------------------------------------------- /testdata/two/file1.go: -------------------------------------------------------------------------------- 1 | package two 2 | 3 | var _ = "file1" 4 | -------------------------------------------------------------------------------- /testdata/two/file2.go: -------------------------------------------------------------------------------- 1 | package two 2 | 3 | var _ = "file2" 4 | -------------------------------------------------------------------------------- /testdata/p1/p3/testp/file1.go: -------------------------------------------------------------------------------- 1 | package testp 2 | 3 | var _ = "file1" 4 | -------------------------------------------------------------------------------- /testdata/p1/testp/file1.go: -------------------------------------------------------------------------------- 1 | package testp 2 | 3 | var _ = "file1" 4 | -------------------------------------------------------------------------------- /testdata/p1/p3/imp1.go: -------------------------------------------------------------------------------- 1 | package p3 2 | 3 | import _ "testdata.tld/util/p1/p2" 4 | -------------------------------------------------------------------------------- /testdata/p1/imp1_test.go: -------------------------------------------------------------------------------- 1 | package p1 2 | 3 | import _ "testdata.tld/util/p1/testp" 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mvdan.cc/gogrep 2 | 3 | require golang.org/x/tools v0.1.0 4 | 5 | go 1.13 6 | -------------------------------------------------------------------------------- /testdata/p1/p3/imp1_test.go: -------------------------------------------------------------------------------- 1 | package p3 2 | 3 | import _ "testdata.tld/util/p1/p3/testp" 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # To prevent CRLF breakages on Windows for fragile files, like testdata. 2 | * -text 3 | -------------------------------------------------------------------------------- /testdata/exprlist.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func foo(a ...int) int 4 | 5 | var _ = foo(1, 2, 3, 4, 5) 6 | -------------------------------------------------------------------------------- /testdata/longstr.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | var _ = `single line` 4 | var _ = `some 5 | multiline 6 | string` 7 | -------------------------------------------------------------------------------- /testdata/p1/imp1.go: -------------------------------------------------------------------------------- 1 | package p1 2 | 3 | import ( 4 | _ "testdata.tld/util/p1/p2" 5 | _ "testdata.tld/util/p1/p3" 6 | ) 7 | -------------------------------------------------------------------------------- /testdata/longstmt.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func foo() 4 | func bar() 5 | 6 | func _() { 7 | if true { 8 | foo() 9 | bar() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.13.x, 1.14.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: go test ./... 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Daniel Martí. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "go/ast" 8 | "go/printer" 9 | "os" 10 | ) 11 | 12 | func (m *matcher) cmdWrite(cmd exprCmd, subs []submatch) []submatch { 13 | seenRoot := make(map[nodePosHash]bool) 14 | filePaths := make(map[*ast.File]string) 15 | var next []submatch 16 | for _, sub := range subs { 17 | root := m.nodeRoot(sub.node) 18 | hash := posHash(root) 19 | if seenRoot[hash] { 20 | continue // avoid dups 21 | } 22 | seenRoot[hash] = true 23 | file, ok := root.(*ast.File) 24 | if ok { 25 | path := m.fset.Position(file.Package).Filename 26 | if path != "" { 27 | // write to disk 28 | filePaths[file] = path 29 | continue 30 | } 31 | } 32 | // pass it on, to print to stdout 33 | next = append(next, submatch{node: root}) 34 | } 35 | for file, path := range filePaths { 36 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0) 37 | if err != nil { 38 | // TODO: return errors instead 39 | panic(err) 40 | } 41 | if err := printConfig.Fprint(f, m.fset, file); err != nil { 42 | // TODO: return errors instead 43 | panic(err) 44 | } 45 | } 46 | return next 47 | } 48 | 49 | var printConfig = printer.Config{ 50 | Mode: printer.UseSpaces | printer.TabIndent, 51 | Tabwidth: 8, 52 | } 53 | 54 | func (m *matcher) nodeRoot(node ast.Node) ast.Node { 55 | parent := m.parentOf(node) 56 | if parent == nil { 57 | return node 58 | } 59 | if _, ok := parent.(nodeList); ok { 60 | return parent 61 | } 62 | return m.nodeRoot(parent) 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gogrep 2 | 3 | GO111MODULE=on go get mvdan.cc/gogrep 4 | 5 | Search for Go code using syntax trees. 6 | 7 | gogrep -x 'if $x != nil { return $x, $*_ }' 8 | 9 | Note that this project is **no longer being developed**. 10 | See https://github.com/mvdan/gogrep/issues/64 for more details. 11 | 12 | ### Instructions 13 | 14 | usage: gogrep commands [packages] 15 | 16 | A command is of the form "-A pattern", where -A is one of: 17 | 18 | -x find all nodes matching a pattern 19 | -g discard nodes not matching a pattern 20 | -v discard nodes matching a pattern 21 | -a filter nodes by certain attributes 22 | -s substitute with a given syntax tree 23 | -w write source back to disk or stdout 24 | 25 | A pattern is a piece of Go code which may include wildcards. It can be: 26 | 27 | a statement (many if split by semicolons) 28 | an expression (many if split by commas) 29 | a type expression 30 | a top-level declaration (var, func, const) 31 | an entire file 32 | 33 | Wildcards consist of `$` and a name. All wildcards with the same name 34 | within an expression must match the same node, excluding "_". Example: 35 | 36 | $x.$_ = $x // assignment of self to a field in self 37 | 38 | If `*` is before the name, it will match any number of nodes. Example: 39 | 40 | fmt.Fprintf(os.Stdout, $*_) // all Fprintfs on stdout 41 | 42 | `*` can also be used to match optional nodes, like: 43 | 44 | for $*_ { $*_ } // will match all for loops 45 | if $*_; $b { $*_ } // will match all ifs with condition $b 46 | 47 | The nodes resulting from applying the commands will be printed line by 48 | line to standard output. 49 | 50 | Here are two simple examples of the -a operand: 51 | 52 | gogrep -x '$x + $y' // will match both numerical and string "+" operations 53 | gogrep -x '$x + $y' -a 'type(string)' // matches only string concatenations 54 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "sort" 9 | "strings" 10 | 11 | "golang.org/x/tools/go/packages" 12 | ) 13 | 14 | func (m *matcher) load(wd string, args ...string) ([]*packages.Package, error) { 15 | mode := packages.NeedName | packages.NeedSyntax | 16 | packages.NeedTypes | packages.NeedTypesInfo 17 | if m.recursive { // need the syntax trees for the dependencies too 18 | mode |= packages.NeedDeps | packages.NeedImports 19 | } 20 | cfg := &packages.Config{ 21 | Mode: mode, 22 | Dir: wd, 23 | Fset: m.fset, 24 | Tests: m.tests, 25 | } 26 | pkgs, err := packages.Load(cfg, args...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | jointErr := "" 31 | packages.Visit(pkgs, nil, func(pkg *packages.Package) { 32 | for _, err := range pkg.Errors { 33 | jointErr += err.Error() + "\n" 34 | } 35 | }) 36 | if jointErr != "" { 37 | return nil, fmt.Errorf("%s", jointErr) 38 | } 39 | 40 | // Make a sorted list of the packages, including transitive dependencies 41 | // if recurse is true. 42 | byPath := make(map[string]*packages.Package) 43 | var addDeps func(*packages.Package) 44 | addDeps = func(pkg *packages.Package) { 45 | if strings.HasSuffix(pkg.PkgPath, ".test") { 46 | // don't add recursive test deps 47 | return 48 | } 49 | for _, imp := range pkg.Imports { 50 | if _, ok := byPath[imp.PkgPath]; ok { 51 | continue // seen; avoid recursive call 52 | } 53 | byPath[imp.PkgPath] = imp 54 | addDeps(imp) 55 | } 56 | } 57 | for _, pkg := range pkgs { 58 | byPath[pkg.PkgPath] = pkg 59 | if m.recursive { 60 | // add all dependencies once 61 | addDeps(pkg) 62 | } 63 | } 64 | pkgs = pkgs[:0] 65 | for _, pkg := range byPath { 66 | pkgs = append(pkgs, pkg) 67 | } 68 | sort.Slice(pkgs, func(i, j int) bool { 69 | return pkgs[i].PkgPath < pkgs[j].PkgPath 70 | }) 71 | return pkgs, nil 72 | } 73 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/build" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | ) 15 | 16 | func TestWriteFiles(t *testing.T) { 17 | argsList := [][]string{ 18 | {"-x", "foo", "-s", "bar"}, 19 | {"-x", "go func() { $f($*a) }()", "-s", "go $f($*a)"}, 20 | } 21 | files := []struct{ orig, want string }{ 22 | { 23 | // TODO: "func foo()" should get changed too 24 | "package p\n\nfunc foo()\nfunc bar()\nfunc f1() { foo() }\n", 25 | "package p\n\nfunc foo()\nfunc bar()\nfunc f1() { bar() }\n", 26 | }, 27 | { 28 | "// package p doc\npackage p\n\nfunc f2() { foo() }\n", 29 | "// package p doc\npackage p\n\nfunc f2() { bar() }\n", 30 | }, 31 | { 32 | `package p 33 | func fn(int) 34 | func f3() { 35 | go func() { 36 | // comment 37 | fn(0) 38 | }() 39 | } 40 | `, 41 | `package p 42 | 43 | func fn(int) 44 | func f3() { 45 | 46 | // comment 47 | go fn(0) 48 | 49 | } 50 | `, 51 | }, 52 | } 53 | dir, err := ioutil.TempDir("", "gogrep-write") 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | defer os.RemoveAll(dir) 58 | var paths []string 59 | for i, file := range files { 60 | path := filepath.Join(dir, fmt.Sprintf("f%02d.go", i)) 61 | if err := ioutil.WriteFile(path, []byte(file.orig), 0644); err != nil { 62 | t.Fatal(err) 63 | } 64 | paths = append(paths, path) 65 | } 66 | for _, args := range argsList { 67 | args = append(args, "-w") 68 | args = append(args, paths...) 69 | 70 | m := matcher{ctx: &build.Default} 71 | var buf bytes.Buffer 72 | m.out = &buf 73 | if err := m.fromArgs(".", args); err != nil { 74 | t.Fatalf("didn't want error, but got %q", err) 75 | } 76 | gotOut := buf.String() 77 | if gotOut != "" { 78 | t.Fatalf("got non-empty output:\n%s", gotOut) 79 | } 80 | } 81 | 82 | for i, path := range paths { 83 | gotBs, err := ioutil.ReadFile(path) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | got := string(gotBs) 88 | want := files[i].want 89 | if got != want { 90 | t.Fatalf("file %d mismatch:\nwant:\n%sgot:\n%s", 91 | i, want, got) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 4 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 5 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 6 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 7 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 10 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 13 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= 16 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 19 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 20 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 21 | golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= 22 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 23 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 26 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/build" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestLoad(t *testing.T) { 16 | ctx := build.Default 17 | baseDir, err := filepath.Abs("testdata") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | m := matcher{ctx: &ctx} 22 | tests := []struct { 23 | args []string 24 | want interface{} 25 | }{ 26 | { 27 | []string{"-x", "var _ = $x", "two/file1.go", "two/file2.go"}, 28 | ` 29 | two/file1.go:3:1: var _ = "file1" 30 | two/file2.go:3:1: var _ = "file2" 31 | `, 32 | }, 33 | // TODO(mvdan): reenable once 34 | // https://github.com/golang/go/issues/29280 is fixed 35 | // { 36 | // []string{"-x", "var _ = $x", "noexist.go"}, 37 | // fmt.Errorf("packages not found"), 38 | // }, 39 | // { 40 | // []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(string)", "noexist.go"}, 41 | // fmt.Errorf("packages not found"), 42 | // }, 43 | { 44 | []string{"-x", "var _ = $x", "./p1"}, 45 | `p1/file1.go:3:1: var _ = "file1"`, 46 | }, 47 | { 48 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(string)", "-p", "2", "./p1"}, 49 | `p1/file1.go:3:1: var _ = "file1"`, 50 | }, 51 | { 52 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(int)", "./p1"}, 53 | ``, // different type 54 | }, 55 | { 56 | []string{"-x", "var _ = $x", "./p1/..."}, 57 | ` 58 | p1/file1.go:3:1: var _ = "file1" 59 | p1/p2/file1.go:3:1: var _ = "file1" 60 | p1/p2/file2.go:3:1: var _ = "file2" 61 | p1/p3/testp/file1.go:3:1: var _ = "file1" 62 | p1/testp/file1.go:3:1: var _ = "file1" 63 | `, 64 | }, 65 | { 66 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(string)", "-p", "2", "./p1/..."}, 67 | ` 68 | p1/file1.go:3:1: var _ = "file1" 69 | p1/p2/file1.go:3:1: var _ = "file1" 70 | p1/p2/file2.go:3:1: var _ = "file2" 71 | p1/p3/testp/file1.go:3:1: var _ = "file1" 72 | p1/testp/file1.go:3:1: var _ = "file1" 73 | `, 74 | }, 75 | { 76 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(string)", "-p", "2", "-r", "./p1"}, 77 | ` 78 | p1/file1.go:3:1: var _ = "file1" 79 | p1/p2/file1.go:3:1: var _ = "file1" 80 | p1/p2/file2.go:3:1: var _ = "file2" 81 | `, 82 | }, 83 | { 84 | []string{"-x", "var _ = $x", "longstr.go"}, 85 | ` 86 | longstr.go:3:1: var _ = ` + "`single line`" + ` 87 | longstr.go:4:1: var _ = "some\nmultiline\nstring" 88 | `, 89 | }, 90 | { 91 | []string{"-x", "if $_ { $*_ }", "longstmt.go"}, 92 | `longstmt.go:7:2: if true { foo(); bar(); }`, 93 | }, 94 | { 95 | []string{"-x", "1, 2, 3, 4, 5", "exprlist.go"}, 96 | `exprlist.go:5:13: 1, 2, 3, 4, 5`, 97 | }, 98 | } 99 | for i, tc := range tests { 100 | t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { 101 | var buf bytes.Buffer 102 | m.out = &buf 103 | err := m.fromArgs(baseDir, tc.args) 104 | switch x := tc.want.(type) { 105 | case error: 106 | if err == nil { 107 | t.Fatalf("wanted error %q, got none", x) 108 | } 109 | want, got := x.Error(), err.Error() 110 | want = filepath.FromSlash(want) 111 | if !strings.Contains(got, want) { 112 | t.Fatalf("wanted error %q, got %q", want, got) 113 | } 114 | case string: 115 | if err != nil { 116 | t.Fatalf("didn't want error, but got %q", err) 117 | } 118 | want := strings.TrimSpace(strings.Replace(x, "\t", "", -1)) 119 | got := strings.TrimSpace(buf.String()) 120 | want = filepath.FromSlash(want) 121 | if want != got { 122 | t.Fatalf("wanted:\n%s\ngot:\n%s", want, got) 123 | } 124 | default: 125 | t.Fatalf("unknown want type %T", x) 126 | } 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /subst.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "go/ast" 9 | "go/token" 10 | "reflect" 11 | ) 12 | 13 | func (m *matcher) cmdSubst(cmd exprCmd, subs []submatch) []submatch { 14 | for i := range subs { 15 | sub := &subs[i] 16 | nodeCopy, _ := m.parseExpr(cmd.src) 17 | // since we'll want to set positions within the file's 18 | // FileSet 19 | scrubPositions(nodeCopy) 20 | 21 | m.fillParents(nodeCopy) 22 | nodeCopy = m.fillValues(nodeCopy, sub.values) 23 | m.substNode(sub.node, nodeCopy) 24 | sub.node = nodeCopy 25 | } 26 | return subs 27 | } 28 | 29 | type topNode struct { 30 | Node ast.Node 31 | } 32 | 33 | func (t topNode) Pos() token.Pos { return t.Node.Pos() } 34 | func (t topNode) End() token.Pos { return t.Node.End() } 35 | 36 | func (m *matcher) fillValues(node ast.Node, values map[string]ast.Node) ast.Node { 37 | // node might not have a parent, in which case we need to set an 38 | // artificial one. Its pointer interface is a copy, so we must also 39 | // return it. 40 | top := &topNode{node} 41 | m.setParentOf(node, top) 42 | 43 | inspect(node, func(node ast.Node) bool { 44 | id := fromWildNode(node) 45 | info := m.info(id) 46 | if info.name == "" { 47 | return true 48 | } 49 | prev := values[info.name] 50 | switch prev.(type) { 51 | case exprList: 52 | node = exprList([]ast.Expr{ 53 | node.(*ast.Ident), 54 | }) 55 | case stmtList: 56 | if ident, ok := node.(*ast.Ident); ok { 57 | node = &ast.ExprStmt{X: ident} 58 | } 59 | node = stmtList([]ast.Stmt{ 60 | node.(*ast.ExprStmt), 61 | }) 62 | } 63 | m.substNode(node, prev) 64 | return true 65 | }) 66 | m.setParentOf(node, nil) 67 | return top.Node 68 | } 69 | 70 | func (m *matcher) substNode(oldNode, newNode ast.Node) { 71 | parent := m.parentOf(oldNode) 72 | m.setParentOf(newNode, parent) 73 | 74 | ptr := m.nodePtr(oldNode) 75 | switch x := ptr.(type) { 76 | case **ast.Ident: 77 | *x = newNode.(*ast.Ident) 78 | case *ast.Node: 79 | *x = newNode 80 | case *ast.Expr: 81 | *x = newNode.(ast.Expr) 82 | case *ast.Stmt: 83 | switch y := newNode.(type) { 84 | case ast.Expr: 85 | stmt := &ast.ExprStmt{X: y} 86 | m.setParentOf(stmt, parent) 87 | *x = stmt 88 | case ast.Stmt: 89 | *x = y 90 | default: 91 | panic(fmt.Sprintf("cannot replace stmt with %T", y)) 92 | } 93 | case *[]ast.Expr: 94 | oldList := oldNode.(exprList) 95 | var first, last []ast.Expr 96 | for i, expr := range *x { 97 | if expr == oldList[0] { 98 | first = (*x)[:i] 99 | last = (*x)[i+len(oldList):] 100 | break 101 | } 102 | } 103 | switch y := newNode.(type) { 104 | case ast.Expr: 105 | *x = append(first, y) 106 | case exprList: 107 | *x = append(first, y...) 108 | default: 109 | panic(fmt.Sprintf("cannot replace exprs with %T", y)) 110 | } 111 | *x = append(*x, last...) 112 | case *[]ast.Stmt: 113 | oldList := oldNode.(stmtList) 114 | var first, last []ast.Stmt 115 | for i, stmt := range *x { 116 | if stmt == oldList[0] { 117 | first = (*x)[:i] 118 | last = (*x)[i+len(oldList):] 119 | break 120 | } 121 | } 122 | switch y := newNode.(type) { 123 | case ast.Expr: 124 | stmt := &ast.ExprStmt{X: y} 125 | m.setParentOf(stmt, parent) 126 | *x = append(first, stmt) 127 | case ast.Stmt: 128 | *x = append(first, y) 129 | case stmtList: 130 | *x = append(first, y...) 131 | default: 132 | panic(fmt.Sprintf("cannot replace stmts with %T", y)) 133 | } 134 | *x = append(*x, last...) 135 | case nil: 136 | return 137 | default: 138 | panic(fmt.Sprintf("unsupported substitution: %T", x)) 139 | } 140 | // the new nodes have scrubbed positions, so try our best to use 141 | // sensible ones 142 | fixPositions(parent) 143 | } 144 | 145 | func (m *matcher) parentOf(node ast.Node) ast.Node { 146 | list, ok := node.(nodeList) 147 | if ok { 148 | node = list.at(0) 149 | } 150 | return m.parents[node] 151 | } 152 | 153 | func (m *matcher) setParentOf(node, parent ast.Node) { 154 | list, ok := node.(nodeList) 155 | if ok { 156 | if list.len() == 0 { 157 | return 158 | } 159 | node = list.at(0) 160 | } 161 | m.parents[node] = parent 162 | } 163 | 164 | func (m *matcher) nodePtr(node ast.Node) interface{} { 165 | list, wantSlice := node.(nodeList) 166 | if wantSlice { 167 | node = list.at(0) 168 | } 169 | parent := m.parentOf(node) 170 | if parent == nil { 171 | return nil 172 | } 173 | v := reflect.ValueOf(parent).Elem() 174 | for i := 0; i < v.NumField(); i++ { 175 | fld := v.Field(i) 176 | switch fld.Type().Kind() { 177 | case reflect.Slice: 178 | for i := 0; i < fld.Len(); i++ { 179 | ifld := fld.Index(i) 180 | if ifld.Interface() != node { 181 | continue 182 | } 183 | if wantSlice { 184 | return fld.Addr().Interface() 185 | } 186 | return ifld.Addr().Interface() 187 | } 188 | case reflect.Interface: 189 | if fld.Interface() == node { 190 | return fld.Addr().Interface() 191 | } 192 | } 193 | } 194 | return nil 195 | } 196 | 197 | // nodePosHash is an ast.Node that can always be used as a key in maps, 198 | // even for nodes that are slices like nodeList. 199 | type nodePosHash struct { 200 | pos, end token.Pos 201 | } 202 | 203 | func (n nodePosHash) Pos() token.Pos { return n.pos } 204 | func (n nodePosHash) End() token.Pos { return n.end } 205 | 206 | func posHash(node ast.Node) nodePosHash { 207 | return nodePosHash{pos: node.Pos(), end: node.End()} 208 | } 209 | 210 | var posType = reflect.TypeOf(token.NoPos) 211 | 212 | func scrubPositions(node ast.Node) { 213 | inspect(node, func(node ast.Node) bool { 214 | v := reflect.ValueOf(node) 215 | if v.Kind() != reflect.Ptr { 216 | return true 217 | } 218 | v = v.Elem() 219 | if v.Kind() != reflect.Struct { 220 | return true 221 | } 222 | for i := 0; i < v.NumField(); i++ { 223 | fld := v.Field(i) 224 | if fld.Type() == posType { 225 | fld.SetInt(0) 226 | } 227 | } 228 | return true 229 | }) 230 | } 231 | 232 | // fixPositions tries to fix common syntax errors caused from syntax rewrites. 233 | func fixPositions(node ast.Node) { 234 | if top, ok := node.(*topNode); ok { 235 | node = top.Node 236 | } 237 | // fallback sets pos to the 'to' position if not valid. 238 | fallback := func(pos *token.Pos, to token.Pos) { 239 | if !pos.IsValid() { 240 | *pos = to 241 | } 242 | } 243 | ast.Inspect(node, func(node ast.Node) bool { 244 | // TODO: many more node types 245 | switch x := node.(type) { 246 | case *ast.GoStmt: 247 | fallback(&x.Go, x.Call.Pos()) 248 | case *ast.ReturnStmt: 249 | if len(x.Results) == 0 { 250 | break 251 | } 252 | // Ensure that there's no newline before the returned 253 | // values, as otherwise we have a naked return. See 254 | // https://github.com/golang/go/issues/32854. 255 | if pos := x.Results[0].Pos(); pos > x.Return { 256 | x.Return = pos 257 | } 258 | } 259 | return true 260 | }) 261 | } 262 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "flag" 9 | "fmt" 10 | "go/ast" 11 | "go/build" 12 | "go/printer" 13 | "go/token" 14 | "go/types" 15 | "io" 16 | "os" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | var usage = func() { 23 | fmt.Fprint(os.Stderr, `usage: gogrep commands [packages] 24 | 25 | gogrep performs a query on the given Go packages. 26 | 27 | -r search dependencies recursively too 28 | -tests search test files too (and direct test deps, with -r) 29 | 30 | A command is one of the following: 31 | 32 | -x pattern find all nodes matching a pattern 33 | -g pattern discard nodes not matching a pattern 34 | -v pattern discard nodes matching a pattern 35 | -a attribute discard nodes without an attribute 36 | -s pattern substitute with a given syntax tree 37 | -p number navigate up a number of node parents 38 | -w write the entire source code back 39 | 40 | A pattern is a piece of Go code which may include dollar expressions. It can be 41 | a number of statements, a number of expressions, a declaration, or an entire 42 | file. 43 | 44 | A dollar expression consist of '$' and a name. Dollar expressions with the same 45 | name within a query always match the same node, excluding "_". Example: 46 | 47 | -x '$x.$_ = $x' # assignment of self to a field in self 48 | 49 | If '*' is before the name, it will match any number of nodes. Example: 50 | 51 | -x 'fmt.Fprintf(os.Stdout, $*_)' # all Fprintfs on stdout 52 | 53 | By default, the resulting nodes will be printed one per line to standard output. 54 | To update the input files, use -w. 55 | `) 56 | } 57 | 58 | func main() { 59 | m := matcher{ 60 | out: os.Stdout, 61 | ctx: &build.Default, 62 | } 63 | err := m.fromArgs(".", os.Args[1:]) 64 | if err != nil { 65 | fmt.Fprintln(os.Stderr, err) 66 | os.Exit(1) 67 | } 68 | } 69 | 70 | type matcher struct { 71 | out io.Writer 72 | ctx *build.Context 73 | 74 | fset *token.FileSet 75 | 76 | parents map[ast.Node]ast.Node 77 | 78 | recursive, tests bool 79 | aggressive bool 80 | 81 | // information about variables (wildcards), by id (which is an 82 | // integer starting at 0) 83 | vars []varInfo 84 | 85 | // node values recorded by name, excluding "_" (used only by the 86 | // actual matching phase) 87 | values map[string]ast.Node 88 | scope *types.Scope 89 | 90 | *types.Info 91 | stdImporter types.Importer 92 | } 93 | 94 | type varInfo struct { 95 | name string 96 | any bool 97 | } 98 | 99 | func (m *matcher) info(id int) varInfo { 100 | if id < 0 { 101 | return varInfo{} 102 | } 103 | return m.vars[id] 104 | } 105 | 106 | type exprCmd struct { 107 | name string 108 | src string 109 | value interface{} 110 | } 111 | 112 | type strCmdFlag struct { 113 | name string 114 | cmds *[]exprCmd 115 | } 116 | 117 | func (o *strCmdFlag) String() string { return "" } 118 | func (o *strCmdFlag) Set(val string) error { 119 | *o.cmds = append(*o.cmds, exprCmd{name: o.name, src: val}) 120 | return nil 121 | } 122 | 123 | type boolCmdFlag struct { 124 | name string 125 | cmds *[]exprCmd 126 | } 127 | 128 | func (o *boolCmdFlag) String() string { return "" } 129 | func (o *boolCmdFlag) Set(val string) error { 130 | if val != "true" { 131 | return fmt.Errorf("flag can only be true") 132 | } 133 | *o.cmds = append(*o.cmds, exprCmd{name: o.name}) 134 | return nil 135 | } 136 | func (o *boolCmdFlag) IsBoolFlag() bool { return true } 137 | 138 | func (m *matcher) fromArgs(wd string, args []string) error { 139 | m.fset = token.NewFileSet() 140 | cmds, args, err := m.parseCmds(args) 141 | if err != nil { 142 | return err 143 | } 144 | pkgs, err := m.load(wd, args...) 145 | if err != nil { 146 | return err 147 | } 148 | var all []ast.Node 149 | for _, pkg := range pkgs { 150 | m.Info = pkg.TypesInfo 151 | nodes := make([]ast.Node, len(pkg.Syntax)) 152 | for i, f := range pkg.Syntax { 153 | nodes[i] = f 154 | } 155 | all = append(all, m.matches(cmds, nodes)...) 156 | } 157 | for _, n := range all { 158 | fpos := m.fset.Position(n.Pos()) 159 | if strings.HasPrefix(fpos.Filename, wd) { 160 | fpos.Filename = fpos.Filename[len(wd)+1:] 161 | } 162 | fmt.Fprintf(m.out, "%v: %s\n", fpos, singleLinePrint(n)) 163 | } 164 | return nil 165 | } 166 | 167 | func (m *matcher) parseCmds(args []string) ([]exprCmd, []string, error) { 168 | flagSet := flag.NewFlagSet("gogrep", flag.ExitOnError) 169 | flagSet.Usage = usage 170 | flagSet.BoolVar(&m.recursive, "r", false, "search dependencies recursively too") 171 | flagSet.BoolVar(&m.tests, "tests", false, "search test files too (and direct test deps, with -r)") 172 | 173 | var cmds []exprCmd 174 | flagSet.Var(&strCmdFlag{ 175 | name: "x", 176 | cmds: &cmds, 177 | }, "x", "") 178 | flagSet.Var(&strCmdFlag{ 179 | name: "g", 180 | cmds: &cmds, 181 | }, "g", "") 182 | flagSet.Var(&strCmdFlag{ 183 | name: "v", 184 | cmds: &cmds, 185 | }, "v", "") 186 | flagSet.Var(&strCmdFlag{ 187 | name: "a", 188 | cmds: &cmds, 189 | }, "a", "") 190 | flagSet.Var(&strCmdFlag{ 191 | name: "s", 192 | cmds: &cmds, 193 | }, "s", "") 194 | flagSet.Var(&strCmdFlag{ 195 | name: "p", 196 | cmds: &cmds, 197 | }, "p", "") 198 | flagSet.Var(&boolCmdFlag{ 199 | name: "w", 200 | cmds: &cmds, 201 | }, "w", "") 202 | flagSet.Parse(args) 203 | paths := flagSet.Args() 204 | 205 | if len(cmds) < 1 { 206 | return nil, nil, fmt.Errorf("need at least one command") 207 | } 208 | for i, cmd := range cmds { 209 | switch cmd.name { 210 | case "w": 211 | continue // no expr 212 | case "p": 213 | n, err := strconv.Atoi(cmd.src) 214 | if err != nil { 215 | return nil, nil, err 216 | } 217 | cmds[i].value = n 218 | case "a": 219 | m, err := m.parseAttrs(cmd.src) 220 | if err != nil { 221 | return nil, nil, fmt.Errorf("cannot parse mods: %v", err) 222 | } 223 | cmds[i].value = m 224 | default: 225 | node, err := m.parseExpr(cmd.src) 226 | if err != nil { 227 | return nil, nil, err 228 | } 229 | cmds[i].value = node 230 | } 231 | } 232 | return cmds, paths, nil 233 | } 234 | 235 | type bufferJoinLines struct { 236 | bytes.Buffer 237 | last string 238 | } 239 | 240 | var rxNeedSemicolon = regexp.MustCompile(`([])}a-zA-Z0-9"'` + "`" + `]|\+\+|--)$`) 241 | 242 | func (b *bufferJoinLines) Write(p []byte) (n int, err error) { 243 | if string(p) == "\n" { 244 | if b.last == "\n" { 245 | return 1, nil 246 | } 247 | if rxNeedSemicolon.MatchString(b.last) { 248 | b.Buffer.WriteByte(';') 249 | } 250 | b.Buffer.WriteByte(' ') 251 | b.last = "\n" 252 | return 1, nil 253 | } 254 | p = bytes.Trim(p, "\t") 255 | n, err = b.Buffer.Write(p) 256 | b.last = string(p) 257 | return 258 | } 259 | 260 | func (b *bufferJoinLines) String() string { 261 | return strings.TrimSuffix(b.Buffer.String(), "; ") 262 | } 263 | 264 | // inspect is like ast.Inspect, but it supports our extra nodeList Node 265 | // type (only at the top level). 266 | func inspect(node ast.Node, fn func(ast.Node) bool) { 267 | // ast.Walk barfs on ast.Node types it doesn't know, so 268 | // do the first level manually here 269 | list, ok := node.(nodeList) 270 | if !ok { 271 | ast.Inspect(node, fn) 272 | return 273 | } 274 | if !fn(list) { 275 | return 276 | } 277 | for i := 0; i < list.len(); i++ { 278 | ast.Inspect(list.at(i), fn) 279 | } 280 | fn(nil) 281 | } 282 | 283 | var emptyFset = token.NewFileSet() 284 | 285 | func singleLinePrint(node ast.Node) string { 286 | var buf bufferJoinLines 287 | inspect(node, func(node ast.Node) bool { 288 | bl, ok := node.(*ast.BasicLit) 289 | if !ok || bl.Kind != token.STRING { 290 | return true 291 | } 292 | if !strings.HasPrefix(bl.Value, "`") { 293 | return true 294 | } 295 | if !strings.Contains(bl.Value, "\n") { 296 | return true 297 | } 298 | bl.Value = strconv.Quote(bl.Value[1 : len(bl.Value)-1]) 299 | return true 300 | }) 301 | printNode(&buf, emptyFset, node) 302 | return buf.String() 303 | } 304 | 305 | func printNode(w io.Writer, fset *token.FileSet, node ast.Node) { 306 | switch x := node.(type) { 307 | case exprList: 308 | if len(x) == 0 { 309 | return 310 | } 311 | printNode(w, fset, x[0]) 312 | for _, n := range x[1:] { 313 | fmt.Fprintf(w, ", ") 314 | printNode(w, fset, n) 315 | } 316 | case stmtList: 317 | if len(x) == 0 { 318 | return 319 | } 320 | printNode(w, fset, x[0]) 321 | for _, n := range x[1:] { 322 | fmt.Fprintf(w, "; ") 323 | printNode(w, fset, n) 324 | } 325 | default: 326 | err := printer.Fprint(w, fset, node) 327 | if err != nil && strings.Contains(err.Error(), "go/printer: unsupported node type") { 328 | // Should never happen, but make it obvious when it does. 329 | panic(fmt.Errorf("cannot print node %T: %v", node, err)) 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/ast" 10 | "go/parser" 11 | "go/scanner" 12 | "go/token" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "text/template" 17 | ) 18 | 19 | func (m *matcher) transformSource(expr string) (string, []posOffset, error) { 20 | toks, err := m.tokenize([]byte(expr)) 21 | if err != nil { 22 | return "", nil, fmt.Errorf("cannot tokenize expr: %v", err) 23 | } 24 | var offs []posOffset 25 | lbuf := lineColBuffer{line: 1, col: 1} 26 | addOffset := func(length int) { 27 | lbuf.offs -= length 28 | offs = append(offs, posOffset{ 29 | atLine: lbuf.line, 30 | atCol: lbuf.col, 31 | offset: length, 32 | }) 33 | } 34 | if len(toks) > 0 && toks[0].tok == tokAggressive { 35 | toks = toks[1:] 36 | m.aggressive = true 37 | } 38 | lastLit := false 39 | for _, t := range toks { 40 | if lbuf.offs >= t.pos.Offset && lastLit && t.lit != "" { 41 | lbuf.WriteString(" ") 42 | } 43 | for lbuf.offs < t.pos.Offset { 44 | lbuf.WriteString(" ") 45 | } 46 | if t.lit == "" { 47 | lbuf.WriteString(t.tok.String()) 48 | lastLit = false 49 | continue 50 | } 51 | if isWildName(t.lit) { 52 | // to correct the position offsets for the extra 53 | // info attached to ident name strings 54 | addOffset(len(wildPrefix) - 1) 55 | } 56 | lbuf.WriteString(t.lit) 57 | lastLit = strings.TrimSpace(t.lit) != "" 58 | } 59 | // trailing newlines can cause issues with commas 60 | return strings.TrimSpace(lbuf.String()), offs, nil 61 | } 62 | 63 | func (m *matcher) parseExpr(expr string) (ast.Node, error) { 64 | exprStr, offs, err := m.transformSource(expr) 65 | if err != nil { 66 | return nil, err 67 | } 68 | node, _, err := parseDetectingNode(m.fset, exprStr) 69 | if err != nil { 70 | err = subPosOffsets(err, offs...) 71 | return nil, fmt.Errorf("cannot parse expr: %v", err) 72 | } 73 | return node, nil 74 | } 75 | 76 | type lineColBuffer struct { 77 | bytes.Buffer 78 | line, col, offs int 79 | } 80 | 81 | func (l *lineColBuffer) WriteString(s string) (n int, err error) { 82 | for _, r := range s { 83 | if r == '\n' { 84 | l.line++ 85 | l.col = 1 86 | } else { 87 | l.col++ 88 | } 89 | l.offs++ 90 | } 91 | return l.Buffer.WriteString(s) 92 | } 93 | 94 | var tmplDecl = template.Must(template.New("").Parse(`` + 95 | `package p; {{ . }}`)) 96 | 97 | var tmplBlock = template.Must(template.New("").Parse(`` + 98 | `package p; func _() { if true {{ . }} else {} }`)) 99 | 100 | var tmplExprs = template.Must(template.New("").Parse(`` + 101 | `package p; var _ = []interface{}{ {{ . }}, }`)) 102 | 103 | var tmplStmts = template.Must(template.New("").Parse(`` + 104 | `package p; func _() { {{ . }} }`)) 105 | 106 | var tmplType = template.Must(template.New("").Parse(`` + 107 | `package p; var _ {{ . }}`)) 108 | 109 | var tmplValSpec = template.Must(template.New("").Parse(`` + 110 | `package p; var {{ . }}`)) 111 | 112 | func execTmpl(tmpl *template.Template, src string) string { 113 | var buf bytes.Buffer 114 | if err := tmpl.Execute(&buf, src); err != nil { 115 | panic(err) 116 | } 117 | return buf.String() 118 | } 119 | 120 | func noBadNodes(node ast.Node) bool { 121 | any := false 122 | ast.Inspect(node, func(n ast.Node) bool { 123 | if any { 124 | return false 125 | } 126 | switch n.(type) { 127 | case *ast.BadExpr, *ast.BadDecl: 128 | any = true 129 | } 130 | return true 131 | }) 132 | return !any 133 | } 134 | 135 | func parseType(fset *token.FileSet, src string) (ast.Expr, *ast.File, error) { 136 | asType := execTmpl(tmplType, src) 137 | f, err := parser.ParseFile(fset, "", asType, 0) 138 | if err != nil { 139 | err = subPosOffsets(err, posOffset{1, 1, 17}) 140 | return nil, nil, err 141 | } 142 | vs := f.Decls[0].(*ast.GenDecl).Specs[0].(*ast.ValueSpec) 143 | return vs.Type, f, nil 144 | } 145 | 146 | // parseDetectingNode tries its best to parse the ast.Node contained in src, as 147 | // one of: *ast.File, ast.Decl, ast.Expr, ast.Stmt, *ast.ValueSpec. 148 | // It also returns the *ast.File used for the parsing, so that the returned node 149 | // can be easily type-checked. 150 | func parseDetectingNode(fset *token.FileSet, src string) (ast.Node, *ast.File, error) { 151 | file := fset.AddFile("", fset.Base(), len(src)) 152 | scan := scanner.Scanner{} 153 | scan.Init(file, []byte(src), nil, 0) 154 | if _, tok, _ := scan.Scan(); tok == token.EOF { 155 | return nil, nil, fmt.Errorf("empty source code") 156 | } 157 | var mainErr error 158 | 159 | // first try as a whole file 160 | if f, err := parser.ParseFile(fset, "", src, 0); err == nil && noBadNodes(f) { 161 | return f, f, nil 162 | } 163 | 164 | // then as a single declaration, or many 165 | asDecl := execTmpl(tmplDecl, src) 166 | if f, err := parser.ParseFile(fset, "", asDecl, 0); err == nil && noBadNodes(f) { 167 | if len(f.Decls) == 1 { 168 | return f.Decls[0], f, nil 169 | } 170 | return f, f, nil 171 | } 172 | 173 | // then as a block; otherwise blocks might be mistaken for composite 174 | // literals further below 175 | asBlock := execTmpl(tmplBlock, src) 176 | if f, err := parser.ParseFile(fset, "", asBlock, 0); err == nil && noBadNodes(f) { 177 | bl := f.Decls[0].(*ast.FuncDecl).Body 178 | if len(bl.List) == 1 { 179 | ifs := bl.List[0].(*ast.IfStmt) 180 | return ifs.Body, f, nil 181 | } 182 | } 183 | 184 | // then as value expressions 185 | asExprs := execTmpl(tmplExprs, src) 186 | if f, err := parser.ParseFile(fset, "", asExprs, 0); err == nil && noBadNodes(f) { 187 | vs := f.Decls[0].(*ast.GenDecl).Specs[0].(*ast.ValueSpec) 188 | cl := vs.Values[0].(*ast.CompositeLit) 189 | if len(cl.Elts) == 1 { 190 | return cl.Elts[0], f, nil 191 | } 192 | return exprList(cl.Elts), f, nil 193 | } 194 | 195 | // then try as statements 196 | asStmts := execTmpl(tmplStmts, src) 197 | if f, err := parser.ParseFile(fset, "", asStmts, 0); err == nil && noBadNodes(f) { 198 | bl := f.Decls[0].(*ast.FuncDecl).Body 199 | if len(bl.List) == 1 { 200 | return bl.List[0], f, nil 201 | } 202 | return stmtList(bl.List), f, nil 203 | } else { 204 | // Statements is what covers most cases, so it will give 205 | // the best overall error message. Show positions 206 | // relative to where the user's code is put in the 207 | // template. 208 | mainErr = subPosOffsets(err, posOffset{1, 1, 22}) 209 | } 210 | 211 | // type expressions not yet picked up, for e.g. chans and interfaces 212 | if typ, f, err := parseType(fset, src); err == nil && noBadNodes(f) { 213 | return typ, f, nil 214 | } 215 | 216 | // value specs 217 | asValSpec := execTmpl(tmplValSpec, src) 218 | if f, err := parser.ParseFile(fset, "", asValSpec, 0); err == nil && noBadNodes(f) { 219 | vs := f.Decls[0].(*ast.GenDecl).Specs[0].(*ast.ValueSpec) 220 | return vs, f, nil 221 | } 222 | return nil, nil, mainErr 223 | } 224 | 225 | type posOffset struct { 226 | atLine, atCol int 227 | offset int 228 | } 229 | 230 | func subPosOffsets(err error, offs ...posOffset) error { 231 | list, ok := err.(scanner.ErrorList) 232 | if !ok { 233 | return err 234 | } 235 | for i, err := range list { 236 | for _, off := range offs { 237 | if err.Pos.Line != off.atLine { 238 | continue 239 | } 240 | if err.Pos.Column < off.atCol { 241 | continue 242 | } 243 | err.Pos.Column -= off.offset 244 | } 245 | list[i] = err 246 | } 247 | return list 248 | } 249 | 250 | const ( 251 | _ token.Token = -iota 252 | tokAggressive 253 | ) 254 | 255 | type fullToken struct { 256 | pos token.Position 257 | tok token.Token 258 | lit string 259 | } 260 | 261 | type caseStatus uint 262 | 263 | const ( 264 | caseNone caseStatus = iota 265 | caseNeedBlock 266 | caseHere 267 | ) 268 | 269 | func (m *matcher) tokenize(src []byte) ([]fullToken, error) { 270 | var s scanner.Scanner 271 | fset := token.NewFileSet() 272 | file := fset.AddFile("", fset.Base(), len(src)) 273 | 274 | var err error 275 | onError := func(pos token.Position, msg string) { 276 | switch msg { // allow certain extra chars 277 | case `illegal character U+0024 '$'`: 278 | case `illegal character U+007E '~'`: 279 | default: 280 | err = fmt.Errorf("%v: %s", pos, msg) 281 | } 282 | } 283 | 284 | // we will modify the input source under the scanner's nose to 285 | // enable some features such as regexes. 286 | s.Init(file, src, onError, scanner.ScanComments) 287 | 288 | next := func() fullToken { 289 | pos, tok, lit := s.Scan() 290 | return fullToken{fset.Position(pos), tok, lit} 291 | } 292 | 293 | caseStat := caseNone 294 | 295 | var toks []fullToken 296 | for t := next(); t.tok != token.EOF; t = next() { 297 | switch t.lit { 298 | case "$": // continues below 299 | case "~": 300 | toks = append(toks, fullToken{t.pos, tokAggressive, ""}) 301 | continue 302 | case "switch", "select", "case": 303 | if t.lit == "case" { 304 | caseStat = caseNone 305 | } else { 306 | caseStat = caseNeedBlock 307 | } 308 | fallthrough 309 | default: // regular Go code 310 | if t.tok == token.LBRACE && caseStat == caseNeedBlock { 311 | caseStat = caseHere 312 | } 313 | toks = append(toks, t) 314 | continue 315 | } 316 | wt, err := m.wildcard(t.pos, next) 317 | if err != nil { 318 | return nil, err 319 | } 320 | if caseStat == caseHere { 321 | toks = append(toks, fullToken{wt.pos, token.IDENT, "case"}) 322 | } 323 | toks = append(toks, wt) 324 | if caseStat == caseHere { 325 | toks = append(toks, fullToken{wt.pos, token.COLON, ""}) 326 | toks = append(toks, fullToken{wt.pos, token.IDENT, "gogrep_body"}) 327 | } 328 | } 329 | return toks, err 330 | } 331 | 332 | func (m *matcher) wildcard(pos token.Position, next func() fullToken) (fullToken, error) { 333 | wt := fullToken{pos, token.IDENT, wildPrefix} 334 | t := next() 335 | var info varInfo 336 | if t.tok == token.MUL { 337 | t = next() 338 | info.any = true 339 | } 340 | if t.tok != token.IDENT { 341 | return wt, fmt.Errorf("%v: $ must be followed by ident, got %v", 342 | t.pos, t.tok) 343 | } 344 | id := len(m.vars) 345 | wt.lit += strconv.Itoa(id) 346 | info.name = t.lit 347 | m.vars = append(m.vars, info) 348 | return wt, nil 349 | } 350 | 351 | type typeCheck struct { 352 | op string // "type", "asgn", "conv" 353 | expr ast.Expr 354 | } 355 | 356 | type attribute struct { 357 | neg bool "!" 358 | 359 | under interface{} 360 | } 361 | 362 | type typProperty string 363 | 364 | type typUnderlying string 365 | 366 | func (m *matcher) parseAttrs(src string) (attribute, error) { 367 | var attr attribute 368 | toks, err := m.tokenize([]byte(src)) 369 | if err != nil { 370 | return attr, err 371 | } 372 | i := -1 373 | var t fullToken 374 | next := func() fullToken { 375 | if i++; i < len(toks) { 376 | return toks[i] 377 | } 378 | return fullToken{tok: token.EOF, pos: t.pos} 379 | } 380 | t = next() 381 | if t.tok == token.NOT { 382 | attr.neg = true 383 | t = next() 384 | } 385 | op := t.lit 386 | switch op { // the ones that don't take args 387 | case "comp", "addr": 388 | if t = next(); t.tok != token.SEMICOLON { 389 | return attr, fmt.Errorf("%v: wanted EOF, got %v", t.pos, t.tok) 390 | } 391 | attr.under = typProperty(op) 392 | return attr, nil 393 | } 394 | opPos := t.pos 395 | if t = next(); t.tok != token.LPAREN { 396 | return attr, fmt.Errorf("%v: wanted (", t.pos) 397 | } 398 | switch op { 399 | case "rx": 400 | t = next() 401 | rxStr, err := strconv.Unquote(t.lit) 402 | if err != nil { 403 | return attr, fmt.Errorf("%v: %v", t.pos, err) 404 | } 405 | if !strings.HasPrefix(rxStr, "^") { 406 | rxStr = "^" + rxStr 407 | } 408 | if !strings.HasSuffix(rxStr, "$") { 409 | rxStr = rxStr + "$" 410 | } 411 | rx, err := regexp.Compile(rxStr) 412 | if err != nil { 413 | return attr, fmt.Errorf("%v: %v", t.pos, err) 414 | } 415 | attr.under = rx 416 | case "type", "asgn", "conv": 417 | t = next() 418 | start := t.pos.Offset 419 | for open := 1; open > 0; t = next() { 420 | switch t.tok { 421 | case token.LPAREN: 422 | open++ 423 | case token.RPAREN: 424 | open-- 425 | case token.EOF: 426 | return attr, fmt.Errorf("%v: expected ) to close (", t.pos) 427 | } 428 | } 429 | end := t.pos.Offset - 1 430 | typeStr := strings.TrimSpace(string(src[start:end])) 431 | fset := token.NewFileSet() 432 | typeExpr, _, err := parseType(fset, typeStr) 433 | if err != nil { 434 | return attr, err 435 | } 436 | attr.under = typeCheck{op, typeExpr} 437 | i -= 2 // since we went past RPAREN above 438 | case "is": 439 | switch t = next(); t.lit { 440 | case "basic", "array", "slice", "struct", "interface", 441 | "pointer", "func", "map", "chan": 442 | default: 443 | return attr, fmt.Errorf("%v: unknown type: %q", t.pos, 444 | t.lit) 445 | } 446 | attr.under = typUnderlying(t.lit) 447 | default: 448 | return attr, fmt.Errorf("%v: unknown op %q", opPos, op) 449 | } 450 | if t = next(); t.tok != token.RPAREN { 451 | return attr, fmt.Errorf("%v: wanted ), got %v", t.pos, t.tok) 452 | } 453 | if t = next(); t.tok != token.SEMICOLON { 454 | return attr, fmt.Errorf("%v: wanted EOF, got %v", t.pos, t.tok) 455 | } 456 | return attr, nil 457 | } 458 | 459 | // using a prefix is good enough for now 460 | const wildPrefix = "gogrep_" 461 | 462 | func isWildName(name string) bool { 463 | return strings.HasPrefix(name, wildPrefix) 464 | } 465 | 466 | func fromWildName(s string) int { 467 | if !isWildName(s) { 468 | return -1 469 | } 470 | n, err := strconv.Atoi(s[len(wildPrefix):]) 471 | if err != nil { 472 | return -1 473 | } 474 | return n 475 | } 476 | -------------------------------------------------------------------------------- /match_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/ast" 10 | "go/importer" 11 | "go/token" 12 | "go/types" 13 | "testing" 14 | ) 15 | 16 | type wantErr string 17 | 18 | func tokErr(msg string) wantErr { return wantErr("cannot tokenize expr: " + msg) } 19 | func modErr(msg string) wantErr { return wantErr("cannot parse mods: " + msg) } 20 | func parseErr(msg string) wantErr { return wantErr("cannot parse expr: " + msg) } 21 | 22 | func TestErrors(t *testing.T) { 23 | tests := []struct { 24 | args []string 25 | want interface{} 26 | }{ 27 | 28 | // expr tokenize errors 29 | {[]string{"-x", "$"}, tokErr(`1:2: $ must be followed by ident, got EOF`)}, 30 | {[]string{"-x", `"`}, tokErr(`1:1: string literal not terminated`)}, 31 | {[]string{"-x", ""}, parseErr(`empty source code`)}, 32 | {[]string{"-x", "\t"}, parseErr(`empty source code`)}, 33 | { 34 | []string{"-x", "$x", "-a", "a"}, 35 | modErr(`1:2: wanted (`), 36 | }, 37 | { 38 | []string{"-x", "$x", "-a", "a("}, 39 | modErr(`1:1: unknown op "a"`), 40 | }, 41 | { 42 | []string{"-x", "$x", "-a", "is(foo)"}, 43 | modErr(`1:4: unknown type: "foo"`), 44 | }, 45 | { 46 | []string{"-x", "$x", "-a", "type("}, 47 | modErr(`1:5: expected ) to close (`), 48 | }, 49 | { 50 | []string{"-x", "$x", "-a", "type({)"}, 51 | modErr(`1:1: expected ';', found '{'`), 52 | }, 53 | { 54 | []string{"-x", "$x", "-a", "type(notType + expr)"}, 55 | modErr(`1:9: expected ';', found '+'`), 56 | }, 57 | { 58 | []string{"-x", "$x", "-a", "comp etc"}, 59 | modErr(`1:6: wanted EOF, got IDENT`), 60 | }, 61 | { 62 | []string{"-x", "$x", "-a", "is(slice) etc"}, 63 | modErr(`1:11: wanted EOF, got IDENT`), 64 | }, 65 | 66 | // expr parse errors 67 | {[]string{"-x", "foo)"}, parseErr(`1:4: expected statement, found ')'`)}, 68 | {[]string{"-x", "{"}, parseErr(`1:4: expected '}', found 'EOF'`)}, 69 | {[]string{"-x", "$x)"}, parseErr(`1:3: expected statement, found ')'`)}, 70 | {[]string{"-x", "$x("}, parseErr(`1:5: expected operand, found '}'`)}, 71 | {[]string{"-x", "$*x)"}, parseErr(`1:4: expected statement, found ')'`)}, 72 | {[]string{"-x", "a\n$x)"}, parseErr(`2:3: expected statement, found ')'`)}, 73 | } 74 | for i, tc := range tests { 75 | t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { 76 | grepTest(t, tc.args, "nosrc", tc.want) 77 | }) 78 | } 79 | } 80 | 81 | func TestMatch(t *testing.T) { 82 | tests := []struct { 83 | args []string 84 | src string 85 | want interface{} 86 | }{ 87 | // basic lits 88 | {[]string{"-x", "123"}, "123", 1}, 89 | {[]string{"-x", "false"}, "true", 0}, 90 | 91 | // wildcards 92 | {[]string{"-x", "$x"}, "rune", 1}, 93 | {[]string{"-x", "foo($x, $x)"}, "foo(1, 2)", 0}, 94 | {[]string{"-x", "foo($_, $_)"}, "foo(1, 2)", 1}, 95 | {[]string{"-x", "foo($x, $y, $y)"}, "foo(1, 2, 2)", 1}, 96 | {[]string{"-x", "$x"}, `"foo"`, 1}, 97 | 98 | // recursion 99 | {[]string{"-x", "$x"}, "a + b", 3}, 100 | {[]string{"-x", "$x + $x"}, "foo(a + a, b + b)", 2}, 101 | {[]string{"-x", "$x"}, "var a int", 4}, 102 | {[]string{"-x", "go foo()"}, "a(); go foo(); a()", 1}, 103 | 104 | // ident regex matches 105 | { 106 | []string{"-x", "$x", "-a", "rx(`foo`)"}, 107 | "bar", 0, 108 | }, 109 | { 110 | []string{"-x", "$x", "-a", "rx(`foo`)"}, 111 | "foo", 1, 112 | }, 113 | { 114 | []string{"-x", "$x", "-a", "rx(`foo`)"}, 115 | "_foo", 0, 116 | }, 117 | { 118 | []string{"-x", "$x", "-a", "rx(`foo`)"}, 119 | "foo_", 0, 120 | }, 121 | { 122 | []string{"-x", "$x", "-a", "rx(`.*foo.*`)"}, 123 | "_foo_", 1, 124 | }, 125 | { 126 | []string{"-x", "$x = $_", "-x", "$x", "-a", "rx(`.*`)"}, 127 | "a = b", 1, 128 | }, 129 | { 130 | []string{"-x", "$x = $_", "-x", "$x", "-a", "rx(`.*`)"}, 131 | "a.field = b", 0, 132 | }, 133 | { 134 | []string{"-x", "$x", "-a", "rx(`.*foo.*`)", "-a", "rx(`.*bar.*`)"}, 135 | "foobar; barfoo; foo; barbar", 2, 136 | }, 137 | 138 | // type equality 139 | { 140 | []string{"-x", "$x", "-a", "type(int)"}, 141 | "var i int", 2, // includes "int" the type 142 | }, 143 | { 144 | []string{"-x", "append($x)", "-x", "$x", "-a", "type([]int)"}, 145 | "var _ = append([]int32{3})", 0, 146 | }, 147 | { 148 | []string{"-x", "append($x)", "-x", "$x", "-a", "type([]int)"}, 149 | "var _ = append([]int{3})", 1, 150 | }, 151 | { 152 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type([2]int)"}, 153 | "var _ = [...]int{1}", 0, 154 | }, 155 | { 156 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type([2]int)"}, 157 | "var _ = [...]int{1, 2}", 1, 158 | }, 159 | { 160 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type([2]int)"}, 161 | "var _ = []int{1, 2}", 0, 162 | }, 163 | { 164 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(*int)"}, 165 | "var _ = int(3)", 0, 166 | }, 167 | { 168 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(*int)"}, 169 | "var _ = new(int)", 1, 170 | }, 171 | { 172 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(io.Reader)"}, 173 | `import "io"; var _ = io.Writer(nil)`, 0, 174 | }, 175 | { 176 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "type(io.Reader)"}, 177 | `import "io"; var _ = io.Reader(nil)`, 1, 178 | }, 179 | { 180 | []string{"-x", "$x", "-a", "type(int)"}, 181 | `type I int; func (i I) p() { print(i) }`, 1, 182 | }, 183 | { 184 | []string{"-x", "$x", "-a", "type(*I)"}, 185 | `type I int; var i *I`, 2, 186 | }, 187 | // TODO 188 | // { 189 | // []string{"-x", "$x", "-a", "type(chan int)"}, 190 | // `ch := make(chan int)`, 2, 191 | // }, 192 | 193 | // type assignability 194 | { 195 | []string{"-x", "const _ = $x", "-x", "$x", "-a", "type(int)"}, 196 | "const _ = 3", 0, 197 | }, 198 | { 199 | []string{"-x", "var $x $_", "-x", "$x", "-a", "type(io.Reader)"}, 200 | `import "os"; var f *os.File`, 0, 201 | }, 202 | { 203 | []string{"-x", "var $x $_", "-x", "$x", "-a", "asgn(io.Reader)"}, 204 | `import "os"; var f *os.File`, 1, 205 | }, 206 | { 207 | []string{"-x", "var $x $_", "-x", "$x", "-a", "asgn(io.Writer)"}, 208 | `import "io"; var r io.Reader`, 0, 209 | }, 210 | { 211 | []string{"-x", "var $_ $_ = $x", "-x", "$x", "-a", "!asgn(*url.URL)"}, 212 | `var _ interface{} = 0`, 1, 213 | }, 214 | { 215 | []string{"-x", "var $_ $_ = $x", "-x", "$x", "-a", "asgn(*url.URL)"}, 216 | `var _ interface{} = nil`, 1, 217 | }, 218 | // TODO: why do these err expressions have invalid types? 219 | // { 220 | // []string{"-x", "err", "-a", "!asgn(error)"}, 221 | // `err := fmt.Errorf("foo")`, 0, 222 | // }, 223 | // { 224 | // []string{"-x", "err", "-a", "asgn(error)"}, 225 | // `err := fmt.Errorf("foo")`, 1, 226 | // }, 227 | // { 228 | // []string{"-x", "err", "-a", "!asgn(error)"}, 229 | // `err := fmt.Sprint("bar")`, 1, 230 | // }, 231 | 232 | // type conversions 233 | { 234 | []string{"-x", "const _ = $x", "-x", "$x", "-a", "type(int)"}, 235 | "const _ = 3", 0, 236 | }, 237 | { 238 | []string{"-x", "const _ = $x", "-x", "$x", "-a", "conv(int)"}, 239 | "const _ = 3", 1, 240 | }, 241 | { 242 | []string{"-x", "const _ = $x", "-x", "$x", "-a", "conv(int32)"}, 243 | "const _ = 3", 1, 244 | }, 245 | { 246 | []string{"-x", "const _ = $x", "-x", "$x", "-a", "conv([]byte)"}, 247 | "const _ = 3", 0, 248 | }, 249 | { 250 | []string{"-x", "var $x $_", "-x", "$x", "-a", "type(int)"}, 251 | "type I int; var i I", 0, 252 | }, 253 | { 254 | []string{"-x", "var $x $_", "-x", "$x", "-a", "conv(int)"}, 255 | "type I int; var i I", 1, 256 | }, 257 | 258 | // comparable types 259 | { 260 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "comp"}, 261 | "var _ = []byte{0}", 0, 262 | }, 263 | { 264 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "comp"}, 265 | "var _ = [...]byte{0}", 1, 266 | }, 267 | 268 | // addressable expressions 269 | { 270 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "addr"}, 271 | "var _ = []byte{0}", 0, 272 | }, 273 | { 274 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "addr"}, 275 | "var s struct { i int }; var _ = s.i", 1, 276 | }, 277 | 278 | // underlying types 279 | { 280 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(basic)"}, 281 | "var _ = []byte{}", 0, 282 | }, 283 | { 284 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(basic)"}, 285 | "var _ = 3", 1, 286 | }, 287 | { 288 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(basic)"}, 289 | `import "io"; var _ = io.SeekEnd`, 1, 290 | }, 291 | { 292 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(array)"}, 293 | "var _ = []byte{}", 0, 294 | }, 295 | { 296 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(array)"}, 297 | "var _ = [...]byte{}", 1, 298 | }, 299 | { 300 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(slice)"}, 301 | "var _ = []byte{}", 1, 302 | }, 303 | { 304 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(slice)"}, 305 | "var _ = [...]byte{}", 0, 306 | }, 307 | { 308 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(struct)"}, 309 | "var _ = []byte{}", 0, 310 | }, 311 | { 312 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(struct)"}, 313 | "var _ = struct{}{}", 1, 314 | }, 315 | { 316 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(interface)"}, 317 | "var _ = struct{}{}", 0, 318 | }, 319 | { 320 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(interface)"}, 321 | "var _ = interface{}(nil)", 1, 322 | }, 323 | { 324 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(pointer)"}, 325 | "var _ = []byte{}", 0, 326 | }, 327 | { 328 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(pointer)"}, 329 | "var _ = new(byte)", 1, 330 | }, 331 | { 332 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(func)"}, 333 | "var _ = []byte{}", 0, 334 | }, 335 | { 336 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(func)"}, 337 | "var _ = func() {}", 1, 338 | }, 339 | { 340 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(map)"}, 341 | "var _ = []byte{}", 0, 342 | }, 343 | { 344 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(map)"}, 345 | "var _ = map[int]int{}", 1, 346 | }, 347 | { 348 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(chan)"}, 349 | "var _ = []byte{}", 0, 350 | }, 351 | { 352 | []string{"-x", "var _ = $x", "-x", "$x", "-a", "is(chan)"}, 353 | "var _ = make(chan int)", 1, 354 | }, 355 | 356 | // many value expressions 357 | {[]string{"-x", "$x, $y"}, "foo(1, 2)", 1}, 358 | {[]string{"-x", "$x, $y"}, "1", 0}, 359 | {[]string{"-x", "$x"}, "a, b", 3}, 360 | // unlike statements, expressions don't automatically 361 | // imply partial matches 362 | {[]string{"-x", "b, c"}, "a, b, c, d", 0}, 363 | {[]string{"-x", "b, c"}, "foo(a, b, c, d)", 0}, 364 | {[]string{"-x", "print($*_, $x)"}, "print(a, b, c)", 1}, 365 | 366 | // any number of expressions 367 | {[]string{"-x", "$*x"}, "a, b", "a, b"}, 368 | {[]string{"-x", "print($*x)"}, "print()", 1}, 369 | {[]string{"-x", "print($*x)"}, "print(a, b)", 1}, 370 | {[]string{"-x", "print($*x, $y, $*z)"}, "print()", 0}, 371 | {[]string{"-x", "print($*x, $y, $*z)"}, "print(a)", 1}, 372 | {[]string{"-x", "print($*x, $y, $*z)"}, "print(a, b, c)", 1}, 373 | {[]string{"-x", "{ $*_; return nil }"}, "{ return nil }", 1}, 374 | {[]string{"-x", "{ $*_; return nil }"}, "{ a(); b(); return nil }", 1}, 375 | {[]string{"-x", "c($*x); c($*x)"}, "c(); c()", 1}, 376 | {[]string{"-x", "c($*x); c()"}, "c(); c()", 1}, 377 | {[]string{"-x", "c($*x); c($*x)"}, "c(x); c(y)", 0}, 378 | {[]string{"-x", "c($*x); c($*x)"}, "c(x, y); c(z)", 0}, 379 | {[]string{"-x", "c($*x); c($*x)"}, "c(x, y); c(x, y)", 1}, 380 | {[]string{"-x", "c($*x, y); c($*x, y)"}, "c(x, y); c(x, y)", 1}, 381 | {[]string{"-x", "c($*x, $*y); c($*x, $*y)"}, "c(x, y); c(x, y)", 1}, 382 | 383 | // composite lits 384 | {[]string{"-x", "[]float64{$x}"}, "[]float64{3}", 1}, 385 | {[]string{"-x", "[2]bool{$x, 0}"}, "[2]bool{3, 1}", 0}, 386 | {[]string{"-x", "someStruct{fld: $x}"}, "someStruct{fld: a, fld2: b}", 0}, 387 | {[]string{"-x", "map[int]int{1: $x}"}, "map[int]int{1: a}", 1}, 388 | 389 | // func lits 390 | {[]string{"-x", "func($s string) { print($s) }"}, "func(a string) { print(a) }", 1}, 391 | {[]string{"-x", "func($x ...$t) {}"}, "func(a ...int) {}", 1}, 392 | 393 | // type exprs 394 | {[]string{"-x", "[8]$x"}, "[8]int", 1}, 395 | {[]string{"-x", "struct{field $t}"}, "struct{field int}", 1}, 396 | {[]string{"-x", "struct{field $t}"}, "struct{field int}", 1}, 397 | {[]string{"-x", "struct{field $t}"}, "struct{other int}", 0}, 398 | {[]string{"-x", "struct{field $t}"}, "struct{f1, f2 int}", 0}, 399 | {[]string{"-x", "interface{$x() int}"}, "interface{i() int}", 1}, 400 | {[]string{"-x", "chan $x"}, "chan bool", 1}, 401 | {[]string{"-x", "<-chan $x"}, "chan bool", 0}, 402 | {[]string{"-x", "chan $x"}, "chan<- bool", 0}, 403 | 404 | // many types (TODO; revisit) 405 | // {[]string{"-x", "chan $x, interface{}"}, "chan int, interface{}", 1}, 406 | // {[]string{"-x", "chan $x, interface{}"}, "chan int", 0}, 407 | // {[]string{"-x", "$x string, $y int"}, "func(s string, i int) {}", 1}, 408 | 409 | // parens 410 | {[]string{"-x", "($x)"}, "(a + b)", 1}, 411 | {[]string{"-x", "($x)"}, "a + b", 0}, 412 | 413 | // unary ops 414 | {[]string{"-x", "-someConst"}, "- someConst", 1}, 415 | {[]string{"-x", "*someVar"}, "* someVar", 1}, 416 | 417 | // binary ops 418 | {[]string{"-x", "$x == $y"}, "a == b", 1}, 419 | {[]string{"-x", "$x == $y"}, "123", 0}, 420 | {[]string{"-x", "$x == $y"}, "a != b", 0}, 421 | {[]string{"-x", "$x - $x"}, "a - b", 0}, 422 | 423 | // calls 424 | {[]string{"-x", "someFunc($x)"}, "someFunc(a > b)", 1}, 425 | 426 | // selector 427 | {[]string{"-x", "$x.Field"}, "a.Field", 1}, 428 | {[]string{"-x", "$x.Field"}, "a.field", 0}, 429 | {[]string{"-x", "$x.Method()"}, "a.Method()", 1}, 430 | {[]string{"-x", "a.b"}, "a.b.c", 1}, 431 | {[]string{"-x", "b.c"}, "a.b.c", 0}, 432 | {[]string{"-x", "$x.c"}, "a.b.c", 1}, 433 | {[]string{"-x", "a.$x"}, "a.b.c", 1}, 434 | 435 | // indexes 436 | {[]string{"-x", "$x[len($x)-1]"}, "a[len(a)-1]", 1}, 437 | {[]string{"-x", "$x[len($x)-1]"}, "a[len(b)-1]", 0}, 438 | 439 | // slicing 440 | {[]string{"-x", "$x[:$y]"}, "a[:1]", 1}, 441 | {[]string{"-x", "$x[3:]"}, "a[3:5:5]", 0}, 442 | 443 | // type asserts 444 | {[]string{"-x", "$x.(string)"}, "a.(string)", 1}, 445 | 446 | // elipsis 447 | {[]string{"-x", "append($x, $y...)"}, "append(a, bs...)", 1}, 448 | {[]string{"-x", "foo($x...)"}, "foo(a)", 0}, 449 | {[]string{"-x", "foo($x...)"}, "foo(a, b)", 0}, 450 | 451 | // forcing node to be a statement 452 | {[]string{"-x", "append($*_);"}, "f(); x = append(x, a)", 0}, 453 | {[]string{"-x", "append($*_);"}, "f(); append(x, a)", 1}, 454 | 455 | // many statements 456 | {[]string{"-x", "$x(); $y()"}, "a(); b()", 1}, 457 | {[]string{"-x", "$x(); $y()"}, "a()", 0}, 458 | {[]string{"-x", "$x"}, "a; b", 3}, 459 | {[]string{"-x", "b; c"}, "b", 0}, 460 | {[]string{"-x", "b; c"}, "b; c", 1}, 461 | {[]string{"-x", "b; c"}, "b; x; c", 0}, 462 | {[]string{"-x", "b; c"}, "a; b; c; d", "b; c"}, 463 | {[]string{"-x", "b; c"}, "{b; c; d}", 1}, 464 | {[]string{"-x", "b; c"}, "{a; b; c}", 1}, 465 | {[]string{"-x", "b; c"}, "{b; b; c; c}", "b; c"}, 466 | {[]string{"-x", "$x++; $x--"}, "n; a++; b++; b--", "b++; b--"}, 467 | {[]string{"-x", "$*_; b; $*_"}, "{a; b; c; d}", "a; b; c; d"}, 468 | {[]string{"-x", "{$*_; $x}"}, "{a; b; c}", 1}, 469 | {[]string{"-x", "{b; c}"}, "{a; b; c}", 0}, 470 | {[]string{"-x", "$x := $_; $x = $_"}, "a := n; b := n; b = m", "b := n; b = m"}, 471 | {[]string{"-x", "$x := $_; $*_; $x = $_"}, "a := n; b := n; b = m", "b := n; b = m"}, 472 | 473 | // mixing lists 474 | {[]string{"-x", "$x, $y"}, "1; 2", 0}, 475 | {[]string{"-x", "$x; $y"}, "1, 2", 0}, 476 | 477 | // any number of statements 478 | {[]string{"-x", "$*x"}, "a; b", "a; b"}, 479 | {[]string{"-x", "$*x; b; $*y"}, "a; b; c", 1}, 480 | {[]string{"-x", "$*x; b; $*x"}, "a; b; c", 0}, 481 | 482 | // const/var declarations 483 | {[]string{"-x", "const $x = $y"}, "const a = b", 1}, 484 | {[]string{"-x", "const $x = $y"}, "const (a = b)", 1}, 485 | {[]string{"-x", "const $x = $y"}, "const (a = b\nc = d)", 0}, 486 | {[]string{"-x", "var $x int"}, "var a int", 1}, 487 | {[]string{"-x", "var $x int"}, "var a int = 3", 0}, 488 | 489 | // func declarations 490 | { 491 | []string{"-x", "func $_($x $y) $y { return $x }"}, 492 | "func a(i int) int { return i }", 1, 493 | }, 494 | {[]string{"-x", "func $x(i int)"}, "func a(i int)", 1}, 495 | {[]string{"-x", "func $x(i int) {}"}, "func a(i int)", 0}, 496 | { 497 | []string{"-x", "func $_() $*_ { $*_ }"}, 498 | "func f() {}", 1, 499 | }, 500 | { 501 | []string{"-x", "func $_() $*_ { $*_ }"}, 502 | "func f() (int, error) { return 3, nil }", 1, 503 | }, 504 | 505 | // type declarations 506 | {[]string{"-x", "struct{}"}, "type T struct{}", 1}, 507 | {[]string{"-x", "type $x struct{}"}, "type T struct{}", 1}, 508 | {[]string{"-x", "struct{$_ int}"}, "type T struct{n int}", 1}, 509 | {[]string{"-x", "struct{$_ int}"}, "var V struct{n int}", 1}, 510 | {[]string{"-x", "struct{$_}"}, "type T struct{n int}", 1}, 511 | {[]string{"-x", "struct{$*_}"}, "type T struct{n int}", 1}, 512 | { 513 | []string{"-x", "struct{$*_; Foo $t; $*_}"}, 514 | "type T struct{Foo string; a int; B}", 1, 515 | }, 516 | // structure literal 517 | {[]string{"-x", "struct{a int}{a: $_}"}, "struct{a int}{a: 1}", 1}, 518 | {[]string{"-x", "struct{a int}{a: $*_}"}, "struct{a int}{a: 1}", 1}, 519 | 520 | // value specs 521 | {[]string{"-x", "$_ int"}, "var a int", 1}, 522 | {[]string{"-x", "$_ int"}, "var a bool", 0}, 523 | // TODO: consider these 524 | {[]string{"-x", "$_ int"}, "var a int = 3", 0}, 525 | {[]string{"-x", "$_ int"}, "var a, b int", 0}, 526 | {[]string{"-x", "$_ int"}, "func(i int) { println(i) }", 0}, 527 | 528 | // entire files 529 | {[]string{"-x", "package $_"}, "package p; var a = 1", 0}, 530 | {[]string{"-x", "package $_; func Foo() { $*_ }"}, "package p; func Foo() {}", 1}, 531 | 532 | // blocks 533 | {[]string{"-x", "{ $x }"}, "{ a() }", 1}, 534 | {[]string{"-x", "{ $x }"}, "{ a(); b() }", 0}, 535 | {[]string{"-x", "{}"}, "func f() {}", 1}, 536 | 537 | // assigns 538 | {[]string{"-x", "$x = $y"}, "a = b", 1}, 539 | {[]string{"-x", "$x := $y"}, "a, b := c()", 0}, 540 | 541 | // if stmts 542 | {[]string{"-x", "if $x != nil { $y }"}, "if p != nil { p.foo() }", 1}, 543 | {[]string{"-x", "if $x { $y }"}, "if a { b() } else { c() }", 0}, 544 | {[]string{"-x", "if $x != nil { $y }"}, "if a != nil { return a }", 1}, 545 | 546 | // for and range stmts 547 | {[]string{"-x", "for $x { $y }"}, "for b { c() }", 1}, 548 | {[]string{"-x", "for $x := range $y { $z }"}, "for i := range l { c() }", 1}, 549 | {[]string{"-x", "for $x := range $y { $z }"}, "for i = range l { c() }", 0}, 550 | {[]string{"-x", "for $x = range $y { $z }"}, "for i := range l { c() }", 0}, 551 | {[]string{"-x", "for range $y { $z }"}, "for _, e := range l { e() }", 0}, 552 | 553 | // $*_ matching stmt+expr combos (ifs) 554 | {[]string{"-x", "if $*x {}"}, "if a {}", 1}, 555 | {[]string{"-x", "if $*x {}"}, "if a(); b {}", 1}, 556 | {[]string{"-x", "if $*x {}; if $*x {}"}, "if a(); b {}; if a(); b {}", 1}, 557 | {[]string{"-x", "if $*x {}; if $*x {}"}, "if a(); b {}; if b {}", 0}, 558 | {[]string{"-x", "if $*_ {} else {}"}, "if a(); b {}", 0}, 559 | {[]string{"-x", "if $*_ {} else {}"}, "if a(); b {} else {}", 1}, 560 | {[]string{"-x", "if a(); $*_ {}"}, "if b {}", 0}, 561 | 562 | // $*_ matching stmt+expr combos (fors) 563 | {[]string{"-x", "for $*x {}"}, "for {}", 1}, 564 | {[]string{"-x", "for $*x {}"}, "for a {}", 1}, 565 | {[]string{"-x", "for $*x {}"}, "for i(); a; p() {}", 1}, 566 | {[]string{"-x", "for $*x {}; for $*x {}"}, "for i(); a; p() {}; for i(); a; p() {}", 1}, 567 | {[]string{"-x", "for $*x {}; for $*x {}"}, "for i(); a; p() {}; for i(); b; p() {}", 0}, 568 | {[]string{"-x", "for a(); $*_; {}"}, "for b {}", 0}, 569 | {[]string{"-x", "for ; $*_; c() {}"}, "for b {}", 0}, 570 | 571 | // $*_ matching stmt+expr combos (switches) 572 | {[]string{"-x", "switch $*x {}"}, "switch a {}", 1}, 573 | {[]string{"-x", "switch $*x {}"}, "switch a(); b {}", 1}, 574 | {[]string{"-x", "switch $*x {}; switch $*x {}"}, "switch a(); b {}; switch a(); b {}", 1}, 575 | {[]string{"-x", "switch $*x {}; switch $*x {}"}, "switch a(); b {}; switch b {}", 0}, 576 | {[]string{"-x", "switch a(); $*_ {}"}, "for b {}", 0}, 577 | 578 | // $*_ matching stmt+expr combos (node type mixing) 579 | {[]string{"-x", "if $*x {}; for $*x {}"}, "if a(); b {}; for a(); b; {}", 1}, 580 | {[]string{"-x", "if $*x {}; for $*x {}"}, "if a(); b {}; for a(); b; c() {}", 0}, 581 | 582 | // for $*_ {} matching a range for 583 | {[]string{"-x", "for $_ {}"}, "for range x {}", 0}, 584 | {[]string{"-x", "for $*_ {}"}, "for range x {}", 1}, 585 | {[]string{"-x", "for $*_ {}"}, "for _, v := range x {}", 1}, 586 | 587 | // $*_ matching optional statements (ifs) 588 | {[]string{"-x", "if $*_; b {}"}, "if b {}", 1}, 589 | {[]string{"-x", "if $*_; b {}"}, "if a := f(); b {}", 1}, 590 | // TODO: should these match? 591 | //{[]string{"-x", "if a(); $*x { f($*x) }"}, "if a(); b { f(b) }", 1}, 592 | //{[]string{"-x", "if a(); $*x { f($*x) }"}, "if a(); b { f(b, c) }", 0}, 593 | //{[]string{"-x", "if $*_; $*_ {}"}, "if a(); b {}", 1}, 594 | 595 | // $*_ matching optional statements (fors) 596 | {[]string{"-x", "for $*x; b; $*x {}"}, "for b {}", 1}, 597 | {[]string{"-x", "for $*x; b; $*x {}"}, "for a(); b; a() {}", 1}, 598 | {[]string{"-x", "for $*x; b; $*x {}"}, "for a(); b; c() {}", 0}, 599 | 600 | // $*_ matching optional statements (switches) 601 | {[]string{"-x", "switch $*_; b {}"}, "switch b := f(); b {}", 1}, 602 | {[]string{"-x", "switch $*_; b {}"}, "switch b := f(); c {}", 0}, 603 | 604 | // inc/dec stmts 605 | {[]string{"-x", "$x++"}, "a[b]++", 1}, 606 | {[]string{"-x", "$x--"}, "a++", 0}, 607 | 608 | // returns 609 | {[]string{"-x", "return nil, $x"}, "{ return nil, err }", 1}, 610 | {[]string{"-x", "return nil, $x"}, "{ return nil, 0, err }", 0}, 611 | 612 | // go stmts 613 | {[]string{"-x", "go $x()"}, "go func() { a() }()", 1}, 614 | {[]string{"-x", "go func() { $x }()"}, "go func() { a() }()", 1}, 615 | {[]string{"-x", "go func() { $x }()"}, "go a()", 0}, 616 | 617 | // defer stmts 618 | {[]string{"-x", "defer $x()"}, "defer func() { a() }()", 1}, 619 | {[]string{"-x", "defer func() { $x }()"}, "defer func() { a() }()", 1}, 620 | {[]string{"-x", "defer func() { $x }()"}, "defer a()", 0}, 621 | 622 | // empty statement 623 | {[]string{"-x", ";"}, ";", 1}, 624 | 625 | // labeled statement 626 | {[]string{"-x", "foo: a"}, "foo: a", 1}, 627 | {[]string{"-x", "foo: a"}, "foo: b", 0}, 628 | 629 | // send statement 630 | {[]string{"-x", "x <- 1"}, "x <- 1", 1}, 631 | {[]string{"-x", "x <- 1"}, "y <- 1", 0}, 632 | {[]string{"-x", "x <- 1"}, "x <- 2", 0}, 633 | 634 | // branch statement 635 | {[]string{"-x", "break foo"}, "break foo", 1}, 636 | {[]string{"-x", "break foo"}, "break bar", 0}, 637 | {[]string{"-x", "break foo"}, "continue foo", 0}, 638 | {[]string{"-x", "break"}, "break", 1}, 639 | {[]string{"-x", "break foo"}, "break", 0}, 640 | 641 | // case clause 642 | {[]string{"-x", "switch x {case 4: x}"}, "switch x {case 4: x}", 1}, 643 | {[]string{"-x", "switch x {case 4: x}"}, "switch y {case 4: x}", 0}, 644 | {[]string{"-x", "switch x {case 4: x}"}, "switch x {case 5: x}", 0}, 645 | {[]string{"-x", "switch {$_}"}, "switch {case 5: x}", 1}, 646 | {[]string{"-x", "switch x {$_}"}, "switch x {case 5: x}", 1}, 647 | {[]string{"-x", "switch x {$*_}"}, "switch x {case 5: x}", 1}, 648 | {[]string{"-x", "switch x {$*_}"}, "switch x {}", 1}, 649 | {[]string{"-x", "switch x {$*_}"}, "switch x {case 1: a; case 2: b}", 1}, 650 | {[]string{"-x", "switch {$a; $a}"}, "switch {case true: a; case true: a}", 1}, 651 | {[]string{"-x", "switch {$a; $a}"}, "switch {case true: a; case true: b}", 0}, 652 | 653 | // switch statement 654 | {[]string{"-x", "switch x; y {}"}, "switch x; y {}", 1}, 655 | {[]string{"-x", "switch x {}"}, "switch x; y {}", 0}, 656 | {[]string{"-x", "switch {}"}, "switch {}", 1}, 657 | {[]string{"-x", "switch {}"}, "switch x {}", 0}, 658 | {[]string{"-x", "switch {}"}, "switch {case y:}", 0}, 659 | {[]string{"-x", "switch $_ {}"}, "switch x {}", 1}, 660 | {[]string{"-x", "switch $_ {}"}, "switch x; y {}", 0}, 661 | {[]string{"-x", "switch $_; $_ {}"}, "switch x {}", 0}, 662 | {[]string{"-x", "switch $_; $_ {}"}, "switch x; y {}", 1}, 663 | {[]string{"-x", "switch { $*_; case $*_: $*a }"}, "switch { case x: y() }", 0}, 664 | 665 | // type switch statement 666 | {[]string{"-x", "switch x := y.(z); x {}"}, "switch x := y.(z); x {}", 1}, 667 | {[]string{"-x", "switch x := y.(z); x {}"}, "switch y := y.(z); x {}", 0}, 668 | {[]string{"-x", "switch x := y.(z); x {}"}, "switch y := y.(z); x {}", 0}, 669 | // TODO more switch variations. 670 | 671 | // TODO select statement 672 | // TODO communication clause 673 | {[]string{"-x", "select {$*_}"}, "select {case <-x: a}", 1}, 674 | {[]string{"-x", "select {$*_}"}, "select {}", 1}, 675 | {[]string{"-x", "select {$a; $a}"}, "select {case <-x: a; case <-x: a}", 1}, 676 | {[]string{"-x", "select {$a; $a}"}, "select {case <-x: a; case <-x: b}", 0}, 677 | {[]string{"-x", "select {case x := <-y: f(x)}"}, "select {case x := <-y: f(x)}", 1}, 678 | 679 | // aggressive mode 680 | {[]string{"-x", "for range $x {}"}, "for _ = range a {}", 0}, 681 | {[]string{"-x", "~ for range $x {}"}, "for _ = range a {}", 1}, 682 | {[]string{"-x", "~ for _ = range $x {}"}, "for range a {}", 1}, 683 | {[]string{"-x", "a int"}, "var (a, b int; c bool)", 0}, 684 | {[]string{"-x", "~ a int"}, "var (a, b uint; c bool)", 0}, 685 | {[]string{"-x", "~ a int"}, "var (a, b int; c bool)", 1}, 686 | {[]string{"-x", "~ a int"}, "var (a, b int; c bool)", 1}, 687 | {[]string{"-x", "{ x; }"}, "switch { case true: x; }", 0}, 688 | {[]string{"-x", "~ { x; }"}, "switch { case true: x; }", 1}, 689 | {[]string{"-x", "a = b"}, "a = b; a := b", 1}, 690 | {[]string{"-x", "a := b"}, "a = b; a := b", 1}, 691 | {[]string{"-x", "~ a = b"}, "a = b; a := b; var a = b", 3}, 692 | {[]string{"-x", "~ a := b"}, "a = b; a := b; var a = b", 3}, 693 | 694 | // many cmds 695 | { 696 | []string{"-x", "break"}, 697 | "switch { case x: break }; for { y(); break; break }", 698 | 3, 699 | }, 700 | { 701 | []string{"-x", "for { $*_ }", "-x", "break"}, 702 | "switch { case x: break }; for { y(); break; break }", 703 | 2, 704 | }, 705 | { 706 | []string{"-x", "for { $*_ }", "-g", "break"}, 707 | "break; for {}; for { if x { break } else { break } }", 708 | 1, 709 | }, 710 | { 711 | []string{"-x", "for { $*_ }", "-v", "break"}, 712 | "break; for {}; for { x() }; for { break }", 713 | 2, 714 | }, 715 | { 716 | []string{"-x", "for { $*sts }", "-x", "$*sts"}, 717 | "for { a(); b() }", 718 | "a(); b()", 719 | }, 720 | { 721 | []string{"-x", "for { $*sts }", "-x", "$*sts"}, 722 | "for { if x { a(); b() } }", 723 | "if x { a(); b(); }", 724 | }, 725 | { 726 | []string{"-x", "foo", "-s", "bar", "-w"}, 727 | `foo(); println("foo"); println(foo, foobar)`, 728 | `bar(); println("foo"); println(bar, foobar)`, 729 | }, 730 | { 731 | []string{"-x", "$f()", "-s", "$f(nil)", "-w"}, 732 | `foo(); bar(); baz(x)`, 733 | `foo(nil); bar(nil); baz(x)`, 734 | }, 735 | { 736 | []string{"-x", "foo($*_)", "-s", "foo()", "-w"}, 737 | `foo(); foo(a, b); bar(x)`, 738 | `foo(); foo(); bar(x)`, 739 | }, 740 | { 741 | []string{"-x", "a, b", "-s", "c, d", "-w"}, 742 | `foo(); foo(a, b); bar(a, b)`, 743 | `foo(); foo(c, d); bar(c, d)`, 744 | }, 745 | { 746 | []string{"-x", "a(); b()", "-s", "c(); d()", "-w"}, 747 | `{ a(); b(); c(); }; { a(); a(); b(); }`, 748 | `{ c(); d(); c(); }; { a(); c(); d(); }`, 749 | }, 750 | { 751 | []string{"-x", "a()", "-s", "c()", "-w"}, 752 | `{ a(); b(); a(); }`, 753 | `{ c(); b(); c(); }`, 754 | }, 755 | { 756 | []string{"-x", "go func() { $f() }()", "-s", "go $f()", "-w"}, 757 | `{ go func() { f.Close() }(); }`, 758 | `{ go f.Close(); }`, 759 | }, 760 | { 761 | []string{"-x", "foo", "-s", "bar", "-w"}, 762 | `package p; var foo int`, 763 | `package p; var bar int`, 764 | }, 765 | { 766 | []string{"-x", "foo($*a)", "-s", "bar($*a)", "-w"}, 767 | `{ foo(); }`, 768 | `{ bar(); }`, 769 | }, 770 | { 771 | []string{"-x", "foo($*a)", "-s", "bar($*a)", "-w"}, 772 | `{ foo(0); }`, 773 | `{ bar(0); }`, 774 | }, 775 | { 776 | []string{"-x", "a(); b()", "-s", "x = a()", "-w"}, 777 | `{ a(); b(); }`, 778 | `{ x = a(); }`, 779 | }, 780 | { 781 | []string{"-x", "a(); b()", "-s", "a()", "-w"}, 782 | `{ a(); b(); }`, 783 | `{ a(); }`, 784 | }, 785 | { 786 | []string{"-x", "a, b", "-s", "c", "-w"}, 787 | `foo(a, b)`, 788 | `foo(c)`, 789 | }, 790 | { 791 | []string{"-x", "b = a()", "-s", "c()", "-w"}, 792 | `if b = a(); b { }`, 793 | `if c(); b { }`, 794 | }, 795 | { 796 | []string{"-x", "foo()", "-p", "1"}, 797 | `{ if foo() { bar(); }; etc(); }`, 798 | `if foo() { bar(); }`, 799 | }, 800 | { 801 | []string{"-x", "f($*a)", "-s", "f2(x, $a)", "-w"}, 802 | `f(c, d)`, 803 | `f2(x, c, d)`, 804 | }, 805 | { 806 | []string{"-x", "err = f(); if err != nil { $*then }", "-s", "if err := f(); err != nil { $then }", "-w"}, 807 | `{ err = f(); if err != nil { handle(err); }; }`, 808 | `{ if err := f(); err != nil { handle(err); }; }`, 809 | }, 810 | { 811 | []string{"-x", "List{$e}", "-s", "$e", "-w"}, 812 | `List{foo()}`, 813 | `foo()`, 814 | }, 815 | } 816 | for i, tc := range tests { 817 | t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { 818 | grepTest(t, tc.args, tc.src, tc.want) 819 | }) 820 | } 821 | } 822 | 823 | type wantMultiline string 824 | 825 | func TestMatchMultiline(t *testing.T) { 826 | tests := []struct { 827 | args []string 828 | src string 829 | want string 830 | }{ 831 | { 832 | []string{"-x", "List{$e}", "-s", "$e", "-w"}, 833 | "return List{\n\tfoo(),\n}", 834 | "return foo()", 835 | }, 836 | } 837 | for i, tc := range tests { 838 | t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { 839 | grepTest(t, tc.args, tc.src, wantMultiline(tc.want)) 840 | }) 841 | } 842 | } 843 | 844 | func grepTest(t *testing.T, args []string, src string, want interface{}) { 845 | tfatalf := func(format string, a ...interface{}) { 846 | t.Fatalf("%v | %q: %s", args, src, fmt.Sprintf(format, a...)) 847 | } 848 | m := matcher{fset: token.NewFileSet()} 849 | cmds, paths, err := m.parseCmds(args) 850 | if len(paths) > 0 { 851 | t.Fatalf("non-zero paths: %v", paths) 852 | } 853 | srcNode, file, srcErr := parseDetectingNode(m.fset, src) 854 | if srcErr != nil { 855 | t.Fatal(srcErr) 856 | } 857 | 858 | // Type-checking is attempted on a best-effort basis. 859 | m.Info = &types.Info{ 860 | Types: make(map[ast.Expr]types.TypeAndValue), 861 | Defs: make(map[*ast.Ident]types.Object), 862 | Uses: make(map[*ast.Ident]types.Object), 863 | Scopes: make(map[ast.Node]*types.Scope), 864 | } 865 | pkg := types.NewPackage("", "") 866 | config := &types.Config{ 867 | Importer: importer.Default(), 868 | Error: func(error) {}, // don't stop at the first error 869 | } 870 | check := types.NewChecker(config, m.fset, pkg, m.Info) 871 | _ = check.Files([]*ast.File{file}) 872 | m.scope = pkg.Scope() 873 | 874 | matches := m.matches(cmds, []ast.Node{srcNode}) 875 | if want, ok := want.(wantErr); ok { 876 | if err == nil { 877 | tfatalf("wanted error %q, got none", want) 878 | } else if got := err.Error(); got != string(want) { 879 | tfatalf("wanted error %q, got %q", want, got) 880 | } 881 | return 882 | } 883 | if err != nil { 884 | tfatalf("unexpected error: %v", err) 885 | } 886 | if want, ok := want.(int); ok { 887 | if len(matches) != want { 888 | tfatalf("wanted %d matches, got %d", want, len(matches)) 889 | } 890 | return 891 | } 892 | if len(matches) != 1 { 893 | tfatalf("wanted 1 match, got %d", len(matches)) 894 | } 895 | var got, wantStr string 896 | switch want := want.(type) { 897 | case string: 898 | wantStr = want 899 | got = singleLinePrint(matches[0]) 900 | case wantMultiline: 901 | wantStr = string(want) 902 | var buf bytes.Buffer 903 | printNode(&buf, m.fset, matches[0]) 904 | got = buf.String() 905 | default: 906 | panic(fmt.Sprintf("unexpected want type: %T", want)) 907 | } 908 | if got != wantStr { 909 | tfatalf("wanted %q, got %q", wantStr, got) 910 | } 911 | } 912 | -------------------------------------------------------------------------------- /match.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Daniel Martí 2 | // See LICENSE for licensing information 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "go/ast" 9 | "go/importer" 10 | "go/token" 11 | "go/types" 12 | "regexp" 13 | "strconv" 14 | ) 15 | 16 | func (m *matcher) matches(cmds []exprCmd, nodes []ast.Node) []ast.Node { 17 | m.parents = make(map[ast.Node]ast.Node) 18 | m.fillParents(nodes...) 19 | initial := make([]submatch, len(nodes)) 20 | for i, node := range nodes { 21 | initial[i].node = node 22 | initial[i].values = make(map[string]ast.Node) 23 | } 24 | final := m.submatches(cmds, initial) 25 | finalNodes := make([]ast.Node, len(final)) 26 | for i := range finalNodes { 27 | finalNodes[i] = final[i].node 28 | } 29 | return finalNodes 30 | } 31 | 32 | func (m *matcher) fillParents(nodes ...ast.Node) { 33 | stack := make([]ast.Node, 1, 32) 34 | for _, node := range nodes { 35 | inspect(node, func(node ast.Node) bool { 36 | if node == nil { 37 | stack = stack[:len(stack)-1] 38 | return true 39 | } 40 | if _, ok := node.(nodeList); !ok { 41 | m.parents[node] = stack[len(stack)-1] 42 | } 43 | stack = append(stack, node) 44 | return true 45 | }) 46 | } 47 | } 48 | 49 | type submatch struct { 50 | node ast.Node 51 | values map[string]ast.Node 52 | } 53 | 54 | func valsCopy(values map[string]ast.Node) map[string]ast.Node { 55 | v2 := make(map[string]ast.Node, len(values)) 56 | for k, v := range values { 57 | v2[k] = v 58 | } 59 | return v2 60 | } 61 | 62 | func (m *matcher) submatches(cmds []exprCmd, subs []submatch) []submatch { 63 | if len(cmds) == 0 { 64 | return subs 65 | } 66 | cmd := cmds[0] 67 | var fn func(exprCmd, []submatch) []submatch 68 | switch cmd.name { 69 | case "x": 70 | fn = m.cmdRange 71 | case "g": 72 | fn = m.cmdFilter(true) 73 | case "v": 74 | fn = m.cmdFilter(false) 75 | case "s": 76 | fn = m.cmdSubst 77 | case "a": 78 | fn = m.cmdAttr 79 | case "p": 80 | fn = m.cmdParents 81 | case "w": 82 | if len(cmds) > 1 { 83 | panic("-w must be the last command") 84 | } 85 | fn = m.cmdWrite 86 | default: 87 | panic(fmt.Sprintf("unknown command: %q", cmd.name)) 88 | } 89 | return m.submatches(cmds[1:], fn(cmd, subs)) 90 | } 91 | 92 | func (m *matcher) cmdRange(cmd exprCmd, subs []submatch) []submatch { 93 | var matches []submatch 94 | seen := map[nodePosHash]bool{} 95 | 96 | // The values context for each new submatch must be a new copy 97 | // from its parent submatch. If we don't do this copy, all the 98 | // submatches would share the same map and have side effects. 99 | var startValues map[string]ast.Node 100 | 101 | match := func(exprNode, node ast.Node) { 102 | if node == nil { 103 | return 104 | } 105 | m.values = valsCopy(startValues) 106 | found := m.topNode(exprNode, node) 107 | if found == nil { 108 | return 109 | } 110 | hash := posHash(found) 111 | if !seen[hash] { 112 | matches = append(matches, submatch{ 113 | node: found, 114 | values: m.values, 115 | }) 116 | seen[hash] = true 117 | } 118 | } 119 | for _, sub := range subs { 120 | startValues = valsCopy(sub.values) 121 | m.walkWithLists(cmd.value.(ast.Node), sub.node, match) 122 | } 123 | return matches 124 | } 125 | 126 | func (m *matcher) cmdFilter(wantAny bool) func(exprCmd, []submatch) []submatch { 127 | return func(cmd exprCmd, subs []submatch) []submatch { 128 | var matches []submatch 129 | any := false 130 | match := func(exprNode, node ast.Node) { 131 | if node == nil { 132 | return 133 | } 134 | found := m.topNode(exprNode, node) 135 | if found != nil { 136 | any = true 137 | } 138 | } 139 | for _, sub := range subs { 140 | any = false 141 | m.values = sub.values 142 | m.walkWithLists(cmd.value.(ast.Node), sub.node, match) 143 | if any == wantAny { 144 | matches = append(matches, sub) 145 | } 146 | } 147 | return matches 148 | } 149 | } 150 | 151 | func (m *matcher) cmdAttr(cmd exprCmd, subs []submatch) []submatch { 152 | var matches []submatch 153 | for _, sub := range subs { 154 | m.values = sub.values 155 | attr := cmd.value.(attribute) 156 | got := m.attrApplies(sub.node, attr.under) 157 | if got == !attr.neg { 158 | matches = append(matches, sub) 159 | } 160 | } 161 | return matches 162 | } 163 | 164 | func (m *matcher) cmdParents(cmd exprCmd, subs []submatch) []submatch { 165 | for i := range subs { 166 | sub := &subs[i] 167 | reps := cmd.value.(int) 168 | for j := 0; j < reps; j++ { 169 | sub.node = m.parentOf(sub.node) 170 | } 171 | } 172 | return subs 173 | } 174 | 175 | func (m *matcher) attrApplies(node ast.Node, attr interface{}) bool { 176 | if rx, ok := attr.(*regexp.Regexp); ok { 177 | if exprStmt, ok := node.(*ast.ExprStmt); ok { 178 | // since we prefer matching entire statements, get the 179 | // ident from the ExprStmt 180 | node = exprStmt.X 181 | } 182 | ident, ok := node.(*ast.Ident) 183 | return ok && rx.MatchString(ident.Name) 184 | } 185 | expr, _ := node.(ast.Expr) 186 | if expr == nil { 187 | return false // only exprs have types 188 | } 189 | t := m.Info.TypeOf(expr) 190 | if t == nil { 191 | return false // an expr, but no type? 192 | } 193 | tv := m.Info.Types[expr] 194 | switch x := attr.(type) { 195 | case typeCheck: 196 | want := m.resolveType(m.scope, x.expr) 197 | switch { 198 | case x.op == "type" && !types.Identical(t, want): 199 | return false 200 | case x.op == "asgn" && !types.AssignableTo(t, want): 201 | return false 202 | case x.op == "conv" && !types.ConvertibleTo(t, want): 203 | return false 204 | } 205 | case typProperty: 206 | switch { 207 | case x == "comp" && !types.Comparable(t): 208 | return false 209 | case x == "addr" && !tv.Addressable(): 210 | return false 211 | } 212 | case typUnderlying: 213 | u := t.Underlying() 214 | uok := true 215 | switch x { 216 | case "basic": 217 | _, uok = u.(*types.Basic) 218 | case "array": 219 | _, uok = u.(*types.Array) 220 | case "slice": 221 | _, uok = u.(*types.Slice) 222 | case "struct": 223 | _, uok = u.(*types.Struct) 224 | case "interface": 225 | _, uok = u.(*types.Interface) 226 | case "pointer": 227 | _, uok = u.(*types.Pointer) 228 | case "func": 229 | _, uok = u.(*types.Signature) 230 | case "map": 231 | _, uok = u.(*types.Map) 232 | case "chan": 233 | _, uok = u.(*types.Chan) 234 | } 235 | if !uok { 236 | return false 237 | } 238 | } 239 | return true 240 | } 241 | 242 | func (m *matcher) walkWithLists(exprNode, node ast.Node, fn func(exprNode, node ast.Node)) { 243 | visit := func(node ast.Node) bool { 244 | fn(exprNode, node) 245 | for _, list := range nodeLists(node) { 246 | fn(exprNode, list) 247 | if id := m.wildAnyIdent(exprNode); id != nil { 248 | // so that "$*a" will match "a, b" 249 | fn(exprList([]ast.Expr{id}), list) 250 | // so that "$*a" will match "a; b" 251 | fn(toStmtList(id), list) 252 | } 253 | } 254 | return true 255 | } 256 | inspect(node, visit) 257 | } 258 | 259 | func (m *matcher) topNode(exprNode, node ast.Node) ast.Node { 260 | sts1, ok1 := exprNode.(stmtList) 261 | sts2, ok2 := node.(stmtList) 262 | if ok1 && ok2 { 263 | // allow a partial match at the top level 264 | return m.nodes(sts1, sts2, true) 265 | } 266 | if m.node(exprNode, node) { 267 | return node 268 | } 269 | return nil 270 | } 271 | 272 | // optNode is like node, but for those nodes that can be nil and are not 273 | // part of a list. For example, init and post statements in a for loop. 274 | func (m *matcher) optNode(expr, node ast.Node) bool { 275 | if ident := m.wildAnyIdent(expr); ident != nil { 276 | if m.node(toStmtList(ident), toStmtList(node)) { 277 | return true 278 | } 279 | } 280 | return m.node(expr, node) 281 | } 282 | 283 | func (m *matcher) node(expr, node ast.Node) bool { 284 | switch node.(type) { 285 | case *ast.File, *ast.FuncType, *ast.BlockStmt, *ast.IfStmt, 286 | *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.CaseClause, 287 | *ast.CommClause, *ast.ForStmt, *ast.RangeStmt: 288 | if scope := m.Info.Scopes[node]; scope != nil { 289 | m.scope = scope 290 | } 291 | } 292 | if !m.aggressive { 293 | if expr == nil || node == nil { 294 | return expr == node 295 | } 296 | } else { 297 | if expr == nil && node == nil { 298 | return true 299 | } 300 | if node == nil { 301 | expr, node = node, expr 302 | } 303 | } 304 | switch x := expr.(type) { 305 | case nil: // only in aggressive mode 306 | y, ok := node.(*ast.Ident) 307 | return ok && y.Name == "_" 308 | 309 | case *ast.File: 310 | y, ok := node.(*ast.File) 311 | if !ok || !m.node(x.Name, y.Name) || len(x.Decls) != len(y.Decls) || 312 | len(x.Imports) != len(y.Imports) { 313 | return false 314 | } 315 | for i, decl := range x.Decls { 316 | if !m.node(decl, y.Decls[i]) { 317 | return false 318 | } 319 | } 320 | for i, imp := range x.Imports { 321 | if !m.node(imp, y.Imports[i]) { 322 | return false 323 | } 324 | } 325 | return true 326 | 327 | case *ast.Ident: 328 | y, yok := node.(*ast.Ident) 329 | if !isWildName(x.Name) { 330 | // not a wildcard 331 | return yok && x.Name == y.Name 332 | } 333 | if _, ok := node.(ast.Node); !ok { 334 | return false // to not include our extra node types 335 | } 336 | id := fromWildName(x.Name) 337 | info := m.info(id) 338 | if info.any { 339 | return false 340 | } 341 | if info.name == "_" { 342 | // values are discarded, matches anything 343 | return true 344 | } 345 | prev, ok := m.values[info.name] 346 | if !ok { 347 | // first occurrence, record value 348 | m.values[info.name] = node 349 | return true 350 | } 351 | // multiple uses must match 352 | return m.node(prev, node) 353 | 354 | // lists (ys are generated by us while walking) 355 | case exprList: 356 | y, ok := node.(exprList) 357 | return ok && m.exprs(x, y) 358 | case stmtList: 359 | y, ok := node.(stmtList) 360 | return ok && m.stmts(x, y) 361 | 362 | // lits 363 | case *ast.BasicLit: 364 | y, ok := node.(*ast.BasicLit) 365 | return ok && x.Kind == y.Kind && x.Value == y.Value 366 | case *ast.CompositeLit: 367 | y, ok := node.(*ast.CompositeLit) 368 | return ok && m.node(x.Type, y.Type) && m.exprs(x.Elts, y.Elts) 369 | case *ast.FuncLit: 370 | y, ok := node.(*ast.FuncLit) 371 | return ok && m.node(x.Type, y.Type) && m.node(x.Body, y.Body) 372 | 373 | // types 374 | case *ast.ArrayType: 375 | y, ok := node.(*ast.ArrayType) 376 | return ok && m.node(x.Len, y.Len) && m.node(x.Elt, y.Elt) 377 | case *ast.MapType: 378 | y, ok := node.(*ast.MapType) 379 | return ok && m.node(x.Key, y.Key) && m.node(x.Value, y.Value) 380 | case *ast.StructType: 381 | y, ok := node.(*ast.StructType) 382 | return ok && m.fields(x.Fields, y.Fields) 383 | case *ast.Field: 384 | // TODO: tags? 385 | y, ok := node.(*ast.Field) 386 | if !ok { 387 | return false 388 | } 389 | if len(x.Names) == 0 && x.Tag == nil && m.node(x.Type, y) { 390 | // Allow $var to match a field. 391 | return true 392 | } 393 | return m.idents(x.Names, y.Names) && m.node(x.Type, y.Type) 394 | case *ast.FuncType: 395 | y, ok := node.(*ast.FuncType) 396 | return ok && m.fields(x.Params, y.Params) && 397 | m.fields(x.Results, y.Results) 398 | case *ast.InterfaceType: 399 | y, ok := node.(*ast.InterfaceType) 400 | return ok && m.fields(x.Methods, y.Methods) 401 | case *ast.ChanType: 402 | y, ok := node.(*ast.ChanType) 403 | return ok && x.Dir == y.Dir && m.node(x.Value, y.Value) 404 | 405 | // other exprs 406 | case *ast.Ellipsis: 407 | y, ok := node.(*ast.Ellipsis) 408 | return ok && m.node(x.Elt, y.Elt) 409 | case *ast.ParenExpr: 410 | y, ok := node.(*ast.ParenExpr) 411 | return ok && m.node(x.X, y.X) 412 | case *ast.UnaryExpr: 413 | y, ok := node.(*ast.UnaryExpr) 414 | return ok && x.Op == y.Op && m.node(x.X, y.X) 415 | case *ast.BinaryExpr: 416 | y, ok := node.(*ast.BinaryExpr) 417 | return ok && x.Op == y.Op && m.node(x.X, y.X) && m.node(x.Y, y.Y) 418 | case *ast.CallExpr: 419 | y, ok := node.(*ast.CallExpr) 420 | return ok && m.node(x.Fun, y.Fun) && m.exprs(x.Args, y.Args) && 421 | bothValid(x.Ellipsis, y.Ellipsis) 422 | case *ast.KeyValueExpr: 423 | y, ok := node.(*ast.KeyValueExpr) 424 | return ok && m.node(x.Key, y.Key) && m.node(x.Value, y.Value) 425 | case *ast.StarExpr: 426 | y, ok := node.(*ast.StarExpr) 427 | return ok && m.node(x.X, y.X) 428 | case *ast.SelectorExpr: 429 | y, ok := node.(*ast.SelectorExpr) 430 | return ok && m.node(x.X, y.X) && m.node(x.Sel, y.Sel) 431 | case *ast.IndexExpr: 432 | y, ok := node.(*ast.IndexExpr) 433 | return ok && m.node(x.X, y.X) && m.node(x.Index, y.Index) 434 | case *ast.SliceExpr: 435 | y, ok := node.(*ast.SliceExpr) 436 | return ok && m.node(x.X, y.X) && m.node(x.Low, y.Low) && 437 | m.node(x.High, y.High) && m.node(x.Max, y.Max) 438 | case *ast.TypeAssertExpr: 439 | y, ok := node.(*ast.TypeAssertExpr) 440 | return ok && m.node(x.X, y.X) && m.node(x.Type, y.Type) 441 | 442 | // decls 443 | case *ast.GenDecl: 444 | y, ok := node.(*ast.GenDecl) 445 | return ok && x.Tok == y.Tok && m.specs(x.Specs, y.Specs) 446 | case *ast.FuncDecl: 447 | y, ok := node.(*ast.FuncDecl) 448 | return ok && m.fields(x.Recv, y.Recv) && m.node(x.Name, y.Name) && 449 | m.node(x.Type, y.Type) && m.node(x.Body, y.Body) 450 | 451 | // specs 452 | case *ast.ValueSpec: 453 | y, ok := node.(*ast.ValueSpec) 454 | if !ok || !m.node(x.Type, y.Type) { 455 | return false 456 | } 457 | if m.aggressive && len(x.Names) == 1 { 458 | for i := range y.Names { 459 | if m.node(x.Names[i], y.Names[i]) && 460 | (x.Values == nil || m.node(x.Values[i], y.Values[i])) { 461 | return true 462 | } 463 | } 464 | } 465 | return m.idents(x.Names, y.Names) && m.exprs(x.Values, y.Values) 466 | 467 | // stmt bridge nodes 468 | case *ast.ExprStmt: 469 | if id, ok := x.X.(*ast.Ident); ok && isWildName(id.Name) { 470 | // prefer matching $x as a statement, as it's 471 | // the parent 472 | return m.node(id, node) 473 | } 474 | y, ok := node.(*ast.ExprStmt) 475 | return ok && m.node(x.X, y.X) 476 | case *ast.DeclStmt: 477 | y, ok := node.(*ast.DeclStmt) 478 | return ok && m.node(x.Decl, y.Decl) 479 | 480 | // stmts 481 | case *ast.EmptyStmt: 482 | _, ok := node.(*ast.EmptyStmt) 483 | return ok 484 | case *ast.LabeledStmt: 485 | y, ok := node.(*ast.LabeledStmt) 486 | return ok && m.node(x.Label, y.Label) && m.node(x.Stmt, y.Stmt) 487 | case *ast.SendStmt: 488 | y, ok := node.(*ast.SendStmt) 489 | return ok && m.node(x.Chan, y.Chan) && m.node(x.Value, y.Value) 490 | case *ast.IncDecStmt: 491 | y, ok := node.(*ast.IncDecStmt) 492 | return ok && x.Tok == y.Tok && m.node(x.X, y.X) 493 | case *ast.AssignStmt: 494 | y, ok := node.(*ast.AssignStmt) 495 | if !m.aggressive { 496 | return ok && x.Tok == y.Tok && 497 | m.exprs(x.Lhs, y.Lhs) && m.exprs(x.Rhs, y.Rhs) 498 | } 499 | if ok { 500 | return m.exprs(x.Lhs, y.Lhs) && m.exprs(x.Rhs, y.Rhs) 501 | } 502 | vs, ok := node.(*ast.ValueSpec) 503 | return ok && m.nodesMatch(exprList(x.Lhs), identList(vs.Names)) && 504 | m.exprs(x.Rhs, vs.Values) 505 | case *ast.GoStmt: 506 | y, ok := node.(*ast.GoStmt) 507 | return ok && m.node(x.Call, y.Call) 508 | case *ast.DeferStmt: 509 | y, ok := node.(*ast.DeferStmt) 510 | return ok && m.node(x.Call, y.Call) 511 | case *ast.ReturnStmt: 512 | y, ok := node.(*ast.ReturnStmt) 513 | return ok && m.exprs(x.Results, y.Results) 514 | case *ast.BranchStmt: 515 | y, ok := node.(*ast.BranchStmt) 516 | return ok && x.Tok == y.Tok && m.node(maybeNilIdent(x.Label), maybeNilIdent(y.Label)) 517 | case *ast.BlockStmt: 518 | if m.aggressive && m.node(stmtList(x.List), node) { 519 | return true 520 | } 521 | y, ok := node.(*ast.BlockStmt) 522 | if !ok { 523 | return false 524 | } 525 | if x == nil || y == nil { 526 | return x == y 527 | } 528 | return m.cases(x.List, y.List) || m.stmts(x.List, y.List) 529 | case *ast.IfStmt: 530 | y, ok := node.(*ast.IfStmt) 531 | if !ok { 532 | return false 533 | } 534 | condAny := m.wildAnyIdent(x.Cond) 535 | if condAny != nil && x.Init == nil { 536 | // if $*x { ... } on the left 537 | left := toStmtList(condAny) 538 | return m.node(left, toStmtList(y.Init, y.Cond)) && 539 | m.node(x.Body, y.Body) && m.optNode(x.Else, y.Else) 540 | } 541 | return m.optNode(x.Init, y.Init) && m.node(x.Cond, y.Cond) && 542 | m.node(x.Body, y.Body) && m.node(x.Else, y.Else) 543 | case *ast.CaseClause: 544 | y, ok := node.(*ast.CaseClause) 545 | return ok && m.exprs(x.List, y.List) && m.stmts(x.Body, y.Body) 546 | case *ast.SwitchStmt: 547 | y, ok := node.(*ast.SwitchStmt) 548 | if !ok { 549 | return false 550 | } 551 | tagAny := m.wildAnyIdent(x.Tag) 552 | if tagAny != nil && x.Init == nil { 553 | // switch $*x { ... } on the left 554 | left := toStmtList(tagAny) 555 | return m.node(left, toStmtList(y.Init, y.Tag)) && 556 | m.node(x.Body, y.Body) 557 | } 558 | return m.optNode(x.Init, y.Init) && m.node(x.Tag, y.Tag) && m.node(x.Body, y.Body) 559 | case *ast.TypeSwitchStmt: 560 | y, ok := node.(*ast.TypeSwitchStmt) 561 | return ok && m.optNode(x.Init, y.Init) && m.node(x.Assign, y.Assign) && m.node(x.Body, y.Body) 562 | case *ast.CommClause: 563 | y, ok := node.(*ast.CommClause) 564 | return ok && m.node(x.Comm, y.Comm) && m.stmts(x.Body, y.Body) 565 | case *ast.SelectStmt: 566 | y, ok := node.(*ast.SelectStmt) 567 | return ok && m.node(x.Body, y.Body) 568 | case *ast.ForStmt: 569 | condIdent := m.wildAnyIdent(x.Cond) 570 | if condIdent != nil && x.Init == nil && x.Post == nil { 571 | // "for $*x { ... }" on the left 572 | left := toStmtList(condIdent) 573 | // also accept RangeStmt on the right 574 | switch y := node.(type) { 575 | case *ast.ForStmt: 576 | return m.node(left, toStmtList(y.Init, y.Cond, y.Post)) && 577 | m.node(x.Body, y.Body) 578 | case *ast.RangeStmt: 579 | return m.node(left, toStmtList(y.Key, y.Value, y.X)) && 580 | m.node(x.Body, y.Body) 581 | default: 582 | return false 583 | } 584 | } 585 | y, ok := node.(*ast.ForStmt) 586 | if !ok { 587 | return false 588 | } 589 | return m.optNode(x.Init, y.Init) && m.node(x.Cond, y.Cond) && 590 | m.optNode(x.Post, y.Post) && m.node(x.Body, y.Body) 591 | case *ast.RangeStmt: 592 | y, ok := node.(*ast.RangeStmt) 593 | if !ok { 594 | return false 595 | } 596 | if !m.aggressive && x.Tok != y.Tok { 597 | return false 598 | } 599 | return m.node(x.Key, y.Key) && m.node(x.Value, y.Value) && 600 | m.node(x.X, y.X) && m.node(x.Body, y.Body) 601 | 602 | case *ast.TypeSpec: 603 | y, ok := node.(*ast.TypeSpec) 604 | return ok && m.node(x.Name, y.Name) && m.node(x.Type, y.Type) 605 | 606 | case *ast.FieldList: 607 | // we ignore these, for now 608 | return false 609 | default: 610 | panic(fmt.Sprintf("unexpected node: %T", x)) 611 | } 612 | } 613 | 614 | func (m *matcher) wildAnyIdent(node ast.Node) *ast.Ident { 615 | switch x := node.(type) { 616 | case *ast.ExprStmt: 617 | return m.wildAnyIdent(x.X) 618 | case *ast.Ident: 619 | if !isWildName(x.Name) { 620 | return nil 621 | } 622 | if !m.info(fromWildName(x.Name)).any { 623 | return nil 624 | } 625 | return x 626 | } 627 | return nil 628 | } 629 | 630 | // resolveType resolves a type expression from a given scope. 631 | func (m *matcher) resolveType(scope *types.Scope, expr ast.Expr) types.Type { 632 | switch x := expr.(type) { 633 | case *ast.Ident: 634 | _, obj := scope.LookupParent(x.Name, token.NoPos) 635 | if obj == nil { 636 | // TODO: error if all resolveType calls on a type 637 | // expression fail? or perhaps resolve type expressions 638 | // across the entire program? 639 | return nil 640 | } 641 | return obj.Type() 642 | case *ast.ArrayType: 643 | elt := m.resolveType(scope, x.Elt) 644 | if x.Len == nil { 645 | return types.NewSlice(elt) 646 | } 647 | bl, ok := x.Len.(*ast.BasicLit) 648 | if !ok || bl.Kind != token.INT { 649 | panic(fmt.Sprintf("TODO: %T", x)) 650 | } 651 | len, _ := strconv.ParseInt(bl.Value, 0, 0) 652 | return types.NewArray(elt, len) 653 | case *ast.StarExpr: 654 | return types.NewPointer(m.resolveType(scope, x.X)) 655 | case *ast.ChanType: 656 | dir := types.SendRecv 657 | switch x.Dir { 658 | case ast.SEND: 659 | dir = types.SendOnly 660 | case ast.RECV: 661 | dir = types.RecvOnly 662 | } 663 | return types.NewChan(dir, m.resolveType(scope, x.Value)) 664 | case *ast.SelectorExpr: 665 | scope = m.findScope(scope, x.X) 666 | return m.resolveType(scope, x.Sel) 667 | default: 668 | panic(fmt.Sprintf("resolveType TODO: %T", x)) 669 | } 670 | } 671 | 672 | func (m *matcher) findScope(scope *types.Scope, expr ast.Expr) *types.Scope { 673 | switch x := expr.(type) { 674 | case *ast.Ident: 675 | _, obj := scope.LookupParent(x.Name, token.NoPos) 676 | if pkg, ok := obj.(*types.PkgName); ok { 677 | return pkg.Imported().Scope() 678 | } 679 | // try to fall back to std 680 | if m.stdImporter == nil { 681 | m.stdImporter = importer.Default() 682 | } 683 | path := x.Name 684 | if longer, ok := stdImportFixes[path]; ok { 685 | path = longer 686 | } 687 | pkg, err := m.stdImporter.Import(path) 688 | if err != nil { 689 | panic(fmt.Sprintf("findScope err: %v", err)) 690 | } 691 | return pkg.Scope() 692 | default: 693 | panic(fmt.Sprintf("findScope TODO: %T", x)) 694 | } 695 | } 696 | 697 | var stdImportFixes = map[string]string{ 698 | // go list std | grep -vE 'vendor|internal' | grep '/' | sed -r 's@^(.*)/([^/]*)$@"\2": "\1/\2",@' | sort 699 | // (after commenting out the less likely duplicates) 700 | "adler32": "hash/adler32", 701 | "aes": "crypto/aes", 702 | "ascii85": "encoding/ascii85", 703 | "asn1": "encoding/asn1", 704 | "ast": "go/ast", 705 | "atomic": "sync/atomic", 706 | "base32": "encoding/base32", 707 | "base64": "encoding/base64", 708 | "big": "math/big", 709 | "binary": "encoding/binary", 710 | "bits": "math/bits", 711 | "build": "go/build", 712 | "bzip2": "compress/bzip2", 713 | "cgi": "net/http/cgi", 714 | "cgo": "runtime/cgo", 715 | "cipher": "crypto/cipher", 716 | "cmplx": "math/cmplx", 717 | "color": "image/color", 718 | "constant": "go/constant", 719 | "cookiejar": "net/http/cookiejar", 720 | "crc32": "hash/crc32", 721 | "crc64": "hash/crc64", 722 | "csv": "encoding/csv", 723 | "debug": "runtime/debug", 724 | "des": "crypto/des", 725 | "doc": "go/doc", 726 | "draw": "image/draw", 727 | "driver": "database/sql/driver", 728 | "dsa": "crypto/dsa", 729 | "dwarf": "debug/dwarf", 730 | "ecdsa": "crypto/ecdsa", 731 | "elf": "debug/elf", 732 | "elliptic": "crypto/elliptic", 733 | "exec": "os/exec", 734 | "fcgi": "net/http/fcgi", 735 | "filepath": "path/filepath", 736 | "flate": "compress/flate", 737 | "fnv": "hash/fnv", 738 | "format": "go/format", 739 | "gif": "image/gif", 740 | "gob": "encoding/gob", 741 | "gosym": "debug/gosym", 742 | "gzip": "compress/gzip", 743 | "heap": "container/heap", 744 | "hex": "encoding/hex", 745 | "hmac": "crypto/hmac", 746 | "http": "net/http", 747 | "httptest": "net/http/httptest", 748 | "httptrace": "net/http/httptrace", 749 | "httputil": "net/http/httputil", 750 | "importer": "go/importer", 751 | "iotest": "testing/iotest", 752 | "ioutil": "io/ioutil", 753 | "jpeg": "image/jpeg", 754 | "json": "encoding/json", 755 | "jsonrpc": "net/rpc/jsonrpc", 756 | "list": "container/list", 757 | "lzw": "compress/lzw", 758 | "macho": "debug/macho", 759 | "mail": "net/mail", 760 | "md5": "crypto/md5", 761 | "multipart": "mime/multipart", 762 | "palette": "image/color/palette", 763 | "parser": "go/parser", 764 | "parse": "text/template/parse", 765 | "pe": "debug/pe", 766 | "pem": "encoding/pem", 767 | "pkix": "crypto/x509/pkix", 768 | "plan9obj": "debug/plan9obj", 769 | "png": "image/png", 770 | //"pprof": "net/http/pprof", 771 | "pprof": "runtime/pprof", 772 | "printer": "go/printer", 773 | "quick": "testing/quick", 774 | "quotedprintable": "mime/quotedprintable", 775 | "race": "runtime/race", 776 | //"rand": "crypto/rand", 777 | "rand": "math/rand", 778 | "rc4": "crypto/rc4", 779 | "ring": "container/ring", 780 | "rpc": "net/rpc", 781 | "rsa": "crypto/rsa", 782 | //"scanner": "go/scanner", 783 | "scanner": "text/scanner", 784 | "sha1": "crypto/sha1", 785 | "sha256": "crypto/sha256", 786 | "sha512": "crypto/sha512", 787 | "signal": "os/signal", 788 | "smtp": "net/smtp", 789 | "sql": "database/sql", 790 | "subtle": "crypto/subtle", 791 | "suffixarray": "index/suffixarray", 792 | "syntax": "regexp/syntax", 793 | "syslog": "log/syslog", 794 | "tabwriter": "text/tabwriter", 795 | "tar": "archive/tar", 796 | //"template": "html/template", 797 | "template": "text/template", 798 | "textproto": "net/textproto", 799 | "tls": "crypto/tls", 800 | "token": "go/token", 801 | "trace": "runtime/trace", 802 | "types": "go/types", 803 | "url": "net/url", 804 | "user": "os/user", 805 | "utf16": "unicode/utf16", 806 | "utf8": "unicode/utf8", 807 | "x509": "crypto/x509", 808 | "xml": "encoding/xml", 809 | "zip": "archive/zip", 810 | "zlib": "compress/zlib", 811 | } 812 | 813 | func maybeNilIdent(x *ast.Ident) ast.Node { 814 | if x == nil { 815 | return nil 816 | } 817 | return x 818 | } 819 | 820 | func bothValid(p1, p2 token.Pos) bool { 821 | return p1.IsValid() == p2.IsValid() 822 | } 823 | 824 | type nodeList interface { 825 | at(i int) ast.Node 826 | len() int 827 | slice(from, to int) nodeList 828 | ast.Node 829 | } 830 | 831 | // nodes matches two lists of nodes. It uses a common algorithm to match 832 | // wildcard patterns with any number of nodes without recursion. 833 | func (m *matcher) nodes(ns1, ns2 nodeList, partial bool) ast.Node { 834 | ns1len, ns2len := ns1.len(), ns2.len() 835 | if ns1len == 0 { 836 | if ns2len == 0 { 837 | return ns2 838 | } 839 | return nil 840 | } 841 | partialStart, partialEnd := 0, ns2len 842 | i1, i2 := 0, 0 843 | next1, next2 := 0, 0 844 | 845 | // We need to keep a copy of m.values so that we can restart 846 | // with a different "any of" match while discarding any matches 847 | // we found while trying it. 848 | type restart struct { 849 | matches map[string]ast.Node 850 | next1, next2 int 851 | } 852 | // We need to stack these because otherwise some edge cases 853 | // would not match properly. Since we have various kinds of 854 | // wildcards (nodes containing them, $_, and $*_), in some cases 855 | // we may have to go back and do multiple restarts to get to the 856 | // right starting position. 857 | var stack []restart 858 | push := func(n1, n2 int) { 859 | if n2 > ns2len { 860 | return // would be discarded anyway 861 | } 862 | stack = append(stack, restart{valsCopy(m.values), n1, n2}) 863 | next1, next2 = n1, n2 864 | } 865 | pop := func() { 866 | i1, i2 = next1, next2 867 | m.values = stack[len(stack)-1].matches 868 | stack = stack[:len(stack)-1] 869 | next1, next2 = 0, 0 870 | if len(stack) > 0 { 871 | next1 = stack[len(stack)-1].next1 872 | next2 = stack[len(stack)-1].next2 873 | } 874 | } 875 | wildName := "" 876 | wildStart := 0 877 | 878 | // wouldMatch returns whether the current wildcard - if any - 879 | // matches the nodes we are currently trying it on. 880 | wouldMatch := func() bool { 881 | switch wildName { 882 | case "", "_": 883 | return true 884 | } 885 | list := ns2.slice(wildStart, i2) 886 | // check that it matches any nodes found elsewhere 887 | prev, ok := m.values[wildName] 888 | if ok && !m.node(prev, list) { 889 | return false 890 | } 891 | m.values[wildName] = list 892 | return true 893 | } 894 | for i1 < ns1len || i2 < ns2len { 895 | if i1 < ns1len { 896 | n1 := ns1.at(i1) 897 | id := fromWildNode(n1) 898 | info := m.info(id) 899 | if info.any { 900 | // keep track of where this wildcard 901 | // started (if info.name == wildName, 902 | // we're trying the same wildcard 903 | // matching one more node) 904 | if info.name != wildName { 905 | wildStart = i2 906 | wildName = info.name 907 | } 908 | // try to match zero or more at i2, 909 | // restarting at i2+1 if it fails 910 | push(i1, i2+1) 911 | i1++ 912 | continue 913 | } 914 | if partial && i1 == 0 { 915 | // let "b; c" match "a; b; c" 916 | // (simulates a $*_ at the beginning) 917 | partialStart = i2 918 | push(i1, i2+1) 919 | } 920 | if i2 < ns2len && wouldMatch() && m.node(n1, ns2.at(i2)) { 921 | wildName = "" 922 | // ordinary match 923 | i1++ 924 | i2++ 925 | continue 926 | } 927 | } 928 | if partial && i1 == ns1len && wildName == "" { 929 | partialEnd = i2 930 | break // let "b; c" match "b; c; d" 931 | } 932 | // mismatch, try to restart 933 | if 0 < next2 && next2 <= ns2len && (i1 != next1 || i2 != next2) { 934 | pop() 935 | continue 936 | } 937 | return nil 938 | } 939 | if !wouldMatch() { 940 | return nil 941 | } 942 | return ns2.slice(partialStart, partialEnd) 943 | } 944 | 945 | func (m *matcher) nodesMatch(list1, list2 nodeList) bool { 946 | return m.nodes(list1, list2, false) != nil 947 | } 948 | 949 | func (m *matcher) exprs(exprs1, exprs2 []ast.Expr) bool { 950 | return m.nodesMatch(exprList(exprs1), exprList(exprs2)) 951 | } 952 | 953 | func (m *matcher) idents(ids1, ids2 []*ast.Ident) bool { 954 | return m.nodesMatch(identList(ids1), identList(ids2)) 955 | } 956 | 957 | func toStmtList(nodes ...ast.Node) stmtList { 958 | var stmts []ast.Stmt 959 | for _, node := range nodes { 960 | switch x := node.(type) { 961 | case nil: 962 | case ast.Stmt: 963 | stmts = append(stmts, x) 964 | case ast.Expr: 965 | stmts = append(stmts, &ast.ExprStmt{X: x}) 966 | default: 967 | panic(fmt.Sprintf("unexpected node type: %T", x)) 968 | } 969 | } 970 | return stmtList(stmts) 971 | } 972 | 973 | func (m *matcher) cases(stmts1, stmts2 []ast.Stmt) bool { 974 | for _, stmt := range stmts2 { 975 | switch stmt.(type) { 976 | case *ast.CaseClause, *ast.CommClause: 977 | default: 978 | return false 979 | } 980 | } 981 | var left []*ast.Ident 982 | for _, stmt := range stmts1 { 983 | var expr ast.Expr 984 | var bstmt ast.Stmt 985 | switch x := stmt.(type) { 986 | case *ast.CaseClause: 987 | if len(x.List) != 1 || len(x.Body) != 1 { 988 | return false 989 | } 990 | expr, bstmt = x.List[0], x.Body[0] 991 | case *ast.CommClause: 992 | if x.Comm == nil || len(x.Body) != 1 { 993 | return false 994 | } 995 | if commExpr, ok := x.Comm.(*ast.ExprStmt); ok { 996 | expr = commExpr.X 997 | } 998 | bstmt = x.Body[0] 999 | default: 1000 | return false 1001 | } 1002 | xs, ok := bstmt.(*ast.ExprStmt) 1003 | if !ok { 1004 | return false 1005 | } 1006 | bodyIdent, ok := xs.X.(*ast.Ident) 1007 | if !ok || bodyIdent.Name != "gogrep_body" { 1008 | return false 1009 | } 1010 | id, ok := expr.(*ast.Ident) 1011 | if !ok || !isWildName(id.Name) { 1012 | return false 1013 | } 1014 | left = append(left, id) 1015 | } 1016 | return m.nodesMatch(identList(left), stmtList(stmts2)) 1017 | } 1018 | 1019 | func (m *matcher) stmts(stmts1, stmts2 []ast.Stmt) bool { 1020 | return m.nodesMatch(stmtList(stmts1), stmtList(stmts2)) 1021 | } 1022 | 1023 | func (m *matcher) specs(specs1, specs2 []ast.Spec) bool { 1024 | return m.nodesMatch(specList(specs1), specList(specs2)) 1025 | } 1026 | 1027 | func (m *matcher) fields(fields1, fields2 *ast.FieldList) bool { 1028 | var list1, list2 fieldList 1029 | if fields1 != nil { 1030 | list1 = fields1.List 1031 | } 1032 | if fields2 != nil { 1033 | list2 = fields2.List 1034 | } 1035 | return m.nodesMatch(list1, list2) 1036 | } 1037 | 1038 | func fromWildNode(node ast.Node) int { 1039 | switch node := node.(type) { 1040 | case *ast.Ident: 1041 | return fromWildName(node.Name) 1042 | case *ast.ExprStmt: 1043 | return fromWildNode(node.X) 1044 | case *ast.Field: 1045 | // Allow $var to represent an entire field; the lone identifier 1046 | // gets picked up as an anonymous field. 1047 | if len(node.Names) == 0 && node.Tag == nil { 1048 | return fromWildNode(node.Type) 1049 | } 1050 | case *ast.KeyValueExpr: 1051 | return fromWildNode(node.Value) 1052 | } 1053 | return -1 1054 | } 1055 | 1056 | func nodeLists(n ast.Node) []nodeList { 1057 | var lists []nodeList 1058 | addList := func(list nodeList) { 1059 | if list.len() > 0 { 1060 | lists = append(lists, list) 1061 | } 1062 | } 1063 | switch x := n.(type) { 1064 | case nodeList: 1065 | addList(x) 1066 | case *ast.CompositeLit: 1067 | addList(exprList(x.Elts)) 1068 | case *ast.CallExpr: 1069 | addList(exprList(x.Args)) 1070 | case *ast.AssignStmt: 1071 | addList(exprList(x.Lhs)) 1072 | addList(exprList(x.Rhs)) 1073 | case *ast.ReturnStmt: 1074 | addList(exprList(x.Results)) 1075 | case *ast.ValueSpec: 1076 | addList(exprList(x.Values)) 1077 | case *ast.BlockStmt: 1078 | addList(stmtList(x.List)) 1079 | case *ast.CaseClause: 1080 | addList(exprList(x.List)) 1081 | addList(stmtList(x.Body)) 1082 | case *ast.CommClause: 1083 | addList(stmtList(x.Body)) 1084 | } 1085 | return lists 1086 | } 1087 | 1088 | type ( 1089 | exprList []ast.Expr 1090 | identList []*ast.Ident 1091 | stmtList []ast.Stmt 1092 | specList []ast.Spec 1093 | fieldList []*ast.Field 1094 | ) 1095 | 1096 | func (l exprList) len() int { return len(l) } 1097 | func (l identList) len() int { return len(l) } 1098 | func (l stmtList) len() int { return len(l) } 1099 | func (l specList) len() int { return len(l) } 1100 | func (l fieldList) len() int { return len(l) } 1101 | 1102 | func (l exprList) at(i int) ast.Node { return l[i] } 1103 | func (l identList) at(i int) ast.Node { return l[i] } 1104 | func (l stmtList) at(i int) ast.Node { return l[i] } 1105 | func (l specList) at(i int) ast.Node { return l[i] } 1106 | func (l fieldList) at(i int) ast.Node { return l[i] } 1107 | 1108 | func (l exprList) slice(i, j int) nodeList { return l[i:j] } 1109 | func (l identList) slice(i, j int) nodeList { return l[i:j] } 1110 | func (l stmtList) slice(i, j int) nodeList { return l[i:j] } 1111 | func (l specList) slice(i, j int) nodeList { return l[i:j] } 1112 | func (l fieldList) slice(i, j int) nodeList { return l[i:j] } 1113 | 1114 | func (l exprList) Pos() token.Pos { return l[0].Pos() } 1115 | func (l identList) Pos() token.Pos { return l[0].Pos() } 1116 | func (l stmtList) Pos() token.Pos { return l[0].Pos() } 1117 | func (l specList) Pos() token.Pos { return l[0].Pos() } 1118 | func (l fieldList) Pos() token.Pos { return l[0].Pos() } 1119 | 1120 | func (l exprList) End() token.Pos { return l[len(l)-1].End() } 1121 | func (l identList) End() token.Pos { return l[len(l)-1].End() } 1122 | func (l stmtList) End() token.Pos { return l[len(l)-1].End() } 1123 | func (l specList) End() token.Pos { return l[len(l)-1].End() } 1124 | func (l fieldList) End() token.Pos { return l[len(l)-1].End() } 1125 | --------------------------------------------------------------------------------