├── 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 | 
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 |
--------------------------------------------------------------------------------