├── README.md ├── go.mod ├── go.sum ├── internal ├── analysis │ ├── analysis.go │ ├── analysis_test.go │ ├── internal │ │ └── visitor │ │ │ └── visitor.go │ ├── testdata │ │ ├── package1 │ │ │ ├── file1.go │ │ │ └── file2.go │ │ └── package2 │ │ │ ├── file1.go │ │ │ └── file2.go │ └── type_info.go └── run │ └── run.go └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # go-expr-completion 2 | 3 | A tool to complete a left-hand side from given expression for Go. 4 |
This tool is aimed to be integrated with text editors. 5 | 6 | ## Usage 7 | 8 | 9 | ### Example 10 | 11 | Let's assume that your `cursor` is on the `F` on line:16 (`fmt.Fprintln`) with this file: 12 | 13 | 14 | `./internal/analysis/testdata/package1/file1.go` 15 | 16 | ```go 17 | package package1 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | ) 24 | 25 | func f1() { 26 | context.Background() 27 | } 28 | 29 | func f2() { 30 | // some 31 | // comments 32 | fmt.Fprintln(os.Stdout, "message") 33 | } 34 | ``` 35 | 36 | The `F` is `130` as byte offsets for the file.
37 | So, let's specify the file path and `-pos 130` argument to `go-expr-completion` like below: 38 | 39 | ```sh 40 | > go-expr-completion -pos 130 -file ./internal/analysis/testdata/package1/file1.go | jq . 41 | 42 | { 43 | "start_pos": 126, 44 | "end_pos": 160, 45 | "values": [ 46 | { 47 | "name": "i", 48 | "type": "int" 49 | }, 50 | { 51 | "name": "err", 52 | "type": "error" 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | Finally, `go-expr-completion` returs type information about `fmt.Fprintln` which is specified by your cursor.
59 | By using this type information in text editors, we can complete the left-hand side from the expression like below (Vim): 60 | 61 | ![01d57234-441d-46ef-bb0d-7b8f336b84b4](https://user-images.githubusercontent.com/2134196/89279213-12ef8100-d682-11ea-8b93-5660b232255d.gif) 62 | 63 | ## Text Editor Plugins 64 | 65 | - Vim 66 | - https://github.com/110y/vim-go-expr-completion 67 | - Emacs 68 | - https://github.com/fujimisakari/emacs-go-expr-completion 69 | - Other Text Editors 70 | - Help Wanted :pray: 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/110y/go-expr-completion 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.7 7 | golang.org/x/tools v0.1.10-0.20220201133613-461d130035e9 8 | ) 9 | 10 | require ( 11 | golang.org/x/mod v0.5.1 // indirect 12 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect 13 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 2 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 3 | golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= 4 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 5 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 6 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | golang.org/x/tools v0.1.10-0.20220201133613-461d130035e9 h1:fdlsPJw/ojotzX35Hphng2azbUAC3t7LBYPsxEstObo= 8 | golang.org/x/tools v0.1.10-0.20220201133613-461d130035e9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 9 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 10 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 11 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 12 | -------------------------------------------------------------------------------- /internal/analysis/analysis.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/ast" 7 | "go/token" 8 | "go/types" 9 | "path/filepath" 10 | "strings" 11 | "unicode" 12 | 13 | "golang.org/x/tools/go/packages" 14 | 15 | "github.com/110y/go-expr-completion/internal/analysis/internal/visitor" 16 | ) 17 | 18 | var ( 19 | pkgConfigMode = packages.NeedName | 20 | packages.NeedFiles | 21 | packages.NeedImports | 22 | packages.NeedSyntax | 23 | packages.NeedTypesInfo | 24 | packages.NeedTypes | 25 | packages.NeedTypesSizes 26 | 27 | specializedTypeVarNameMap = map[string]string{ 28 | "bool": "ok", 29 | "error": "err", 30 | "context.Context": "ctx", 31 | } 32 | ) 33 | 34 | func GetExprTypeInfo(ctx context.Context, path string, pos int) (*TypeInfo, error) { 35 | fs := token.NewFileSet() 36 | 37 | fpath, err := filepath.Abs(path) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to get abs file path: %w", err) 40 | } 41 | 42 | cfg := &packages.Config{ 43 | Context: ctx, 44 | Fset: fs, 45 | Dir: filepath.Dir(fpath), 46 | Mode: pkgConfigMode, 47 | Tests: true, 48 | } 49 | pkgs, err := packages.Load(cfg) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to load package: %w", err) 52 | } 53 | 54 | var pkgIdx int 55 | var fIdx int 56 | for i, pkg := range pkgs { 57 | for j, f := range pkg.GoFiles { 58 | if fpath == f { 59 | fIdx = j 60 | pkgIdx = i 61 | } 62 | } 63 | } 64 | 65 | pkg := pkgs[pkgIdx] 66 | f := pkg.Syntax[fIdx] 67 | 68 | v := visitor.New(pos, fs, pkg.TypesInfo) 69 | ast.Walk(v, f) 70 | 71 | t := v.GetType() 72 | if t == nil { 73 | return nil, nil 74 | } 75 | 76 | tf := &TypeInfo{ 77 | StartPos: v.GetStartPos(), 78 | EndPos: v.GetEndPos(), 79 | Values: createTypeInfo(t), 80 | } 81 | 82 | return tf, nil 83 | } 84 | 85 | func createTypeInfo(t types.Type) []*Value { 86 | switch t := t.(type) { 87 | case *types.Tuple: 88 | result := make([]*Value, t.Len()) 89 | for i := 0; i < t.Len(); i++ { 90 | result[i] = createTypeInfo(t.At(i).Type())[0] 91 | } 92 | return result 93 | 94 | case *types.Chan: 95 | return []*Value{ 96 | { 97 | Name: "ch", 98 | Type: t.String(), 99 | }, 100 | } 101 | 102 | default: 103 | s := t.String() 104 | n, ok := specializedTypeVarNameMap[s] 105 | if ok { 106 | return []*Value{ 107 | { 108 | Name: n, 109 | Type: s, 110 | }, 111 | } 112 | } 113 | 114 | n = strings.TrimPrefix(strings.TrimPrefix(s, "[]"), "*") 115 | 116 | splited := strings.Split(n, ".") 117 | if len(splited) > 1 { 118 | n = splited[len(splited)-1] 119 | } 120 | 121 | return []*Value{ 122 | { 123 | Name: lowerFirstChar(n), 124 | Type: t.String(), 125 | }, 126 | } 127 | } 128 | } 129 | 130 | func lowerFirstChar(str string) string { 131 | for _, s := range str { 132 | return string(unicode.ToLower(s)) 133 | } 134 | 135 | return "" 136 | } 137 | -------------------------------------------------------------------------------- /internal/analysis/analysis_test.go: -------------------------------------------------------------------------------- 1 | package analysis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | 10 | "github.com/110y/go-expr-completion/internal/analysis" 11 | ) 12 | 13 | func TestGetExprTypeInfo(t *testing.T) { 14 | t.Parallel() 15 | 16 | tests := map[string]struct { 17 | path string 18 | pos int 19 | expected *analysis.TypeInfo 20 | }{ 21 | "function call which returns single context.Context variable": { 22 | path: "package1/file1.go", 23 | pos: 67, 24 | expected: &analysis.TypeInfo{ 25 | StartPos: 67, 26 | EndPos: 87, 27 | Values: []*analysis.Value{ 28 | { 29 | Name: "ctx", 30 | Type: "context.Context", 31 | }, 32 | }, 33 | }, 34 | }, 35 | "function call which returns two variables": { 36 | path: "package1/file1.go", 37 | pos: 160, 38 | expected: &analysis.TypeInfo{ 39 | StartPos: 126, 40 | EndPos: 160, 41 | Values: []*analysis.Value{ 42 | { 43 | Name: "i", 44 | Type: "int", 45 | }, 46 | { 47 | Name: "err", 48 | Type: "error", 49 | }, 50 | }, 51 | }, 52 | }, 53 | "pos is in the function literal": { 54 | path: "package1/file2.go", 55 | pos: 166, 56 | expected: &analysis.TypeInfo{ 57 | StartPos: 164, 58 | EndPos: 188, 59 | Values: []*analysis.Value{ 60 | { 61 | Name: "v", 62 | Type: "*github.com/110y/go-expr-completion/internal/analysis/internal/visitor.Visitor", 63 | }, 64 | }, 65 | }, 66 | }, 67 | "pos is in the function arguments": { 68 | path: "package1/file2.go", 69 | pos: 242, 70 | expected: &analysis.TypeInfo{ 71 | StartPos: 222, 72 | EndPos: 246, 73 | Values: []*analysis.Value{ 74 | { 75 | Name: "v", 76 | Type: "*github.com/110y/go-expr-completion/internal/analysis/internal/visitor.Visitor", 77 | }, 78 | }, 79 | }, 80 | }, 81 | "map": { 82 | path: "package2/file1.go", 83 | pos: 56, 84 | expected: &analysis.TypeInfo{ 85 | StartPos: 56, 86 | EndPos: 64, 87 | Values: []*analysis.Value{ 88 | { 89 | Name: "s", 90 | Type: "string", 91 | }, 92 | { 93 | Name: "ok", 94 | Type: "bool", 95 | }, 96 | }, 97 | }, 98 | }, 99 | "map field": { 100 | path: "package2/file1.go", 101 | pos: 163, 102 | expected: &analysis.TypeInfo{ 103 | StartPos: 163, 104 | EndPos: 180, 105 | Values: []*analysis.Value{ 106 | { 107 | Name: "i", 108 | Type: "interface{}", 109 | }, 110 | { 111 | Name: "ok", 112 | Type: "bool", 113 | }, 114 | }, 115 | }, 116 | }, 117 | "nested map field": { 118 | path: "package2/file1.go", 119 | pos: 163, 120 | expected: &analysis.TypeInfo{ 121 | StartPos: 163, 122 | EndPos: 180, 123 | Values: []*analysis.Value{ 124 | { 125 | Name: "i", 126 | Type: "interface{}", 127 | }, 128 | { 129 | Name: "ok", 130 | Type: "bool", 131 | }, 132 | }, 133 | }, 134 | }, 135 | "type assertion": { 136 | path: "package2/file2.go", 137 | pos: 146, 138 | expected: &analysis.TypeInfo{ 139 | StartPos: 146, 140 | EndPos: 166, 141 | Values: []*analysis.Value{ 142 | { 143 | Name: "v", 144 | Type: "*github.com/110y/go-expr-completion/internal/analysis/internal/visitor.Visitor", 145 | }, 146 | { 147 | Name: "ok", 148 | Type: "bool", 149 | }, 150 | }, 151 | }, 152 | }, 153 | "receiver channel": { 154 | path: "package2/file2.go", 155 | pos: 203, 156 | expected: &analysis.TypeInfo{ 157 | StartPos: 183, 158 | EndPos: 206, 159 | Values: []*analysis.Value{ 160 | { 161 | Name: "ch", 162 | Type: "<-chan struct{}", 163 | }, 164 | }, 165 | }, 166 | }, 167 | "sender channel": { 168 | path: "package2/file2.go", 169 | pos: 208, 170 | expected: &analysis.TypeInfo{ 171 | StartPos: 208, 172 | EndPos: 229, 173 | Values: []*analysis.Value{ 174 | { 175 | Name: "ch", 176 | Type: "chan<- struct{}", 177 | }, 178 | }, 179 | }, 180 | }, 181 | "send recv channel": { 182 | path: "package2/file2.go", 183 | pos: 231, 184 | expected: &analysis.TypeInfo{ 185 | StartPos: 231, 186 | EndPos: 260, 187 | Values: []*analysis.Value{ 188 | { 189 | Name: "ch", 190 | Type: "chan struct{}", 191 | }, 192 | }, 193 | }, 194 | }, 195 | } 196 | 197 | for name, test := range tests { 198 | test := test 199 | t.Run(name, func(t *testing.T) { 200 | t.Parallel() 201 | 202 | path := fmt.Sprintf("testdata/%s", test.path) 203 | actual, err := analysis.GetExprTypeInfo(context.Background(), path, test.pos) 204 | if err != nil { 205 | t.Fatalf("error: %s\n", err.Error()) 206 | } 207 | 208 | if diff := cmp.Diff(test.expected, actual); diff != "" { 209 | t.Errorf("\n(-expected, +actual)\n%s", diff) 210 | } 211 | }) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /internal/analysis/internal/visitor/visitor.go: -------------------------------------------------------------------------------- 1 | package visitor 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | ) 8 | 9 | var _ ast.Visitor = (*Visitor)(nil) 10 | 11 | func New(pos int, fs *token.FileSet, info *types.Info) *Visitor { 12 | return &Visitor{ 13 | fileset: fs, 14 | cursorPos: pos, 15 | info: info, 16 | } 17 | } 18 | 19 | type Visitor struct { 20 | cursorPos int 21 | fileset *token.FileSet 22 | info *types.Info 23 | types types.Type 24 | startPos int 25 | endPos int 26 | 27 | inFuncDecl bool 28 | inFuncDeclBody bool 29 | } 30 | 31 | func (v *Visitor) Visit(node ast.Node) ast.Visitor { 32 | if node == nil { 33 | return nil 34 | } 35 | 36 | defer func() { 37 | switch node.(type) { 38 | case *ast.FuncDecl, *ast.FuncLit: 39 | v.inFuncDecl = true 40 | v.inFuncDeclBody = false 41 | case *ast.BlockStmt: 42 | if v.inFuncDecl { 43 | v.inFuncDeclBody = true 44 | } 45 | } 46 | }() 47 | 48 | startPos := v.getPositionOffset(node.Pos()) 49 | endPos := v.getPositionOffset(node.End()) 50 | 51 | if v.cursorPos < startPos || v.cursorPos > endPos { 52 | return nil 53 | } 54 | 55 | expr, ok := node.(ast.Expr) 56 | if !ok { 57 | return v 58 | } 59 | 60 | if v.inFuncDecl && v.inFuncDeclBody { 61 | tv, ok := v.info.Types[expr] 62 | if !ok { 63 | return v 64 | } 65 | 66 | updated := false 67 | 68 | // Map 69 | if idxExpr, ok := expr.(*ast.IndexExpr); ok { 70 | if t, ok := v.info.Types[idxExpr.X]; ok { 71 | if _, ok := t.Type.(*types.Map); ok { 72 | v1 := types.NewVar(token.NoPos, nil, "", tv.Type) 73 | v2 := types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool]) 74 | 75 | v.types = types.NewTuple(v1, v2) 76 | updated = true 77 | } 78 | } 79 | } 80 | 81 | // Type Assertion 82 | if _, ok := expr.(*ast.TypeAssertExpr); ok { 83 | v1 := types.NewVar(token.NoPos, nil, "", tv.Type) 84 | v2 := types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool]) 85 | 86 | v.types = types.NewTuple(v1, v2) 87 | updated = true 88 | } 89 | 90 | if !updated { 91 | v.types = tv.Type 92 | } 93 | 94 | v.startPos = startPos 95 | v.endPos = endPos 96 | 97 | v.inFuncDecl = false 98 | v.inFuncDeclBody = false 99 | } 100 | 101 | return v 102 | } 103 | 104 | func (v *Visitor) GetType() types.Type { 105 | return v.types 106 | } 107 | 108 | func (v *Visitor) GetStartPos() int { 109 | return v.startPos 110 | } 111 | 112 | func (v *Visitor) GetEndPos() int { 113 | return v.endPos 114 | } 115 | 116 | func (v *Visitor) getPositionOffset(pos token.Pos) int { 117 | return v.fileset.Position(pos).Offset 118 | } 119 | -------------------------------------------------------------------------------- /internal/analysis/testdata/package1/file1.go: -------------------------------------------------------------------------------- 1 | package package1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func f1() { 10 | context.Background() 11 | } 12 | 13 | func f2() { 14 | // some 15 | // comments 16 | fmt.Fprintln(os.Stdout, "message") 17 | } 18 | -------------------------------------------------------------------------------- /internal/analysis/testdata/package1/file2.go: -------------------------------------------------------------------------------- 1 | // package1 2 | // package comments 3 | package package1 4 | 5 | import ( 6 | "github.com/110y/go-expr-completion/internal/analysis/internal/visitor" 7 | ) 8 | 9 | func f1() { 10 | f := func() { 11 | visitor.New(0, nil, nil) 12 | } 13 | } 14 | 15 | func f2() { 16 | f3(func() { 17 | visitor.New(0, nil, nil) 18 | }) 19 | } 20 | 21 | func f3(f func()) { 22 | f() 23 | } 24 | -------------------------------------------------------------------------------- /internal/analysis/testdata/package2/file1.go: -------------------------------------------------------------------------------- 1 | package package2 2 | 3 | func f1() { 4 | var m map[string]string 5 | m["key"] 6 | } 7 | 8 | type foo struct { 9 | foo *foo 10 | mapField map[string]interface{} 11 | } 12 | 13 | func f2() { 14 | m := &foo{} 15 | m.mapField["key"] 16 | m.foo.foo.foo.mapField["key"] 17 | } 18 | -------------------------------------------------------------------------------- /internal/analysis/testdata/package2/file2.go: -------------------------------------------------------------------------------- 1 | package package2 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/110y/go-expr-completion/internal/analysis/internal/visitor" 7 | ) 8 | 9 | func f1() { 10 | var v ast.Visitor 11 | v.(*visitor.Visitor) 12 | } 13 | 14 | func f2() { 15 | returnChannelReceiver() 16 | returnChannelSender() 17 | returnChannelSenderReceiver() 18 | } 19 | 20 | func returnChannelReceiver() <-chan struct{} { 21 | return nil 22 | } 23 | 24 | func returnChannelSender() chan<- struct{} { 25 | return nil 26 | } 27 | 28 | func returnChannelSenderReceiver() chan struct{} { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/analysis/type_info.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | type TypeInfo struct { 4 | StartPos int `json:"start_pos"` 5 | EndPos int `json:"end_pos"` 6 | Values []*Value `json:"values"` 7 | } 8 | 9 | type Value struct { 10 | Name string `json:"name"` 11 | Type string `json:"type"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/run/run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/110y/go-expr-completion/internal/analysis" 11 | ) 12 | 13 | func Run() { 14 | os.Exit(run(context.Background())) 15 | } 16 | 17 | func run(ctx context.Context) int { 18 | var pos int 19 | flag.IntVar(&pos, "pos", 0, "position of cursor") 20 | 21 | var path string 22 | flag.StringVar(&path, "file", "", "file") 23 | 24 | flag.Parse() 25 | 26 | f, err := os.Open(path) 27 | if err != nil { 28 | fmt.Fprintf(os.Stderr, "failed to open file: %s", err.Error()) 29 | return 1 30 | } 31 | 32 | if pos == 0 { 33 | // TODO: log 34 | fmt.Fprintf(os.Stderr, "must pass pos") 35 | return 1 36 | } 37 | 38 | types, err := analysis.GetExprTypeInfo(ctx, f.Name(), pos) 39 | if err != nil { 40 | // TODO: log 41 | fmt.Fprintf(os.Stderr, "failed to execute: %s", err.Error()) 42 | return 1 43 | } 44 | 45 | if types == nil { 46 | fmt.Fprint(os.Stderr, "pos not in any Expressions") 47 | return 1 48 | } 49 | 50 | j, err := json.Marshal(types) 51 | if err != nil { 52 | fmt.Fprintf(os.Stderr, "failed to marshal results to json: %s", err.Error()) 53 | return 1 54 | } 55 | 56 | fmt.Fprint(os.Stdout, string(j)) 57 | 58 | return 0 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/110y/go-expr-completion/internal/run" 4 | 5 | func main() { 6 | run.Run() 7 | } 8 | --------------------------------------------------------------------------------