1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package oracle
6
7import (
8	"fmt"
9	"go/ast"
10	"go/build"
11	"go/token"
12	"os"
13	"path/filepath"
14	"sort"
15	"strings"
16
17	"github.com/visualfc/gotools/oracle/oracle/serial"
18	"golang.org/x/tools/go/ast/astutil"
19)
20
21// what reports all the information about the query selection that can be
22// obtained from parsing only its containing source file.
23// It is intended to be a very low-latency query callable from GUI
24// tools, e.g. to populate a menu of options of slower queries about
25// the selected location.
26//
27func what(q *Query) error {
28	qpos, err := fastQueryPos(q.Pos)
29	if err != nil {
30		return err
31	}
32	q.Fset = qpos.fset
33
34	// (ignore errors)
35	srcdir, importPath, _ := guessImportPath(q.Fset.File(qpos.start).Name(), q.Build)
36
37	// Determine which query modes are applicable to the selection.
38	enable := map[string]bool{
39		"describe": true, // any syntax; always enabled
40	}
41
42	if qpos.end > qpos.start {
43		enable["freevars"] = true // nonempty selection?
44	}
45
46	for _, n := range qpos.path {
47		switch n := n.(type) {
48		case *ast.Ident:
49			enable["definition"] = true
50			enable["referrers"] = true
51			enable["implements"] = true
52		case *ast.CallExpr:
53			enable["callees"] = true
54		case *ast.FuncDecl:
55			enable["callers"] = true
56			enable["callstack"] = true
57		case *ast.SendStmt:
58			enable["peers"] = true
59		case *ast.UnaryExpr:
60			if n.Op == token.ARROW {
61				enable["peers"] = true
62			}
63		}
64
65		// For implements, we approximate findInterestingNode.
66		if _, ok := enable["implements"]; !ok {
67			switch n.(type) {
68			case *ast.ArrayType,
69				*ast.StructType,
70				*ast.FuncType,
71				*ast.InterfaceType,
72				*ast.MapType,
73				*ast.ChanType:
74				enable["implements"] = true
75			}
76		}
77
78		// For pointsto, we approximate findInterestingNode.
79		if _, ok := enable["pointsto"]; !ok {
80			switch n.(type) {
81			case ast.Stmt,
82				*ast.ArrayType,
83				*ast.StructType,
84				*ast.FuncType,
85				*ast.InterfaceType,
86				*ast.MapType,
87				*ast.ChanType:
88				enable["pointsto"] = false // not an expr
89
90			case ast.Expr, ast.Decl, *ast.ValueSpec:
91				enable["pointsto"] = true // an expr, maybe
92
93			default:
94				// Comment, Field, KeyValueExpr, etc: ascend.
95			}
96		}
97	}
98
99	// If we don't have an exact selection, disable modes that need one.
100	if !qpos.exact {
101		enable["callees"] = false
102		enable["pointsto"] = false
103		enable["whicherrs"] = false
104		enable["describe"] = false
105	}
106
107	var modes []string
108	for mode := range enable {
109		modes = append(modes, mode)
110	}
111	sort.Strings(modes)
112
113	q.result = &whatResult{
114		path:       qpos.path,
115		srcdir:     srcdir,
116		importPath: importPath,
117		modes:      modes,
118	}
119	return nil
120}
121
122// guessImportPath finds the package containing filename, and returns
123// its source directory (an element of $GOPATH) and its import path
124// relative to it.
125//
126// TODO(adonovan): what about _test.go files that are not part of the
127// package?
128//
129func guessImportPath(filename string, buildContext *build.Context) (srcdir, importPath string, err error) {
130	absFile, err := filepath.Abs(filename)
131	if err != nil {
132		err = fmt.Errorf("can't form absolute path of %s", filename)
133		return
134	}
135	absFileDir := segments(filepath.Dir(absFile))
136
137	// Find the innermost directory in $GOPATH that encloses filename.
138	minD := 1024
139	for _, gopathDir := range buildContext.SrcDirs() {
140		absDir, err := filepath.Abs(gopathDir)
141		if err != nil {
142			continue // e.g. non-existent dir on $GOPATH
143		}
144		d := prefixLen(segments(absDir), absFileDir)
145		// If there are multiple matches,
146		// prefer the innermost enclosing directory
147		// (smallest d).
148		if d >= 0 && d < minD {
149			minD = d
150			srcdir = gopathDir
151			importPath = strings.Join(absFileDir[len(absFileDir)-minD:], string(os.PathSeparator))
152		}
153	}
154	if srcdir == "" {
155		err = fmt.Errorf("directory %s is not beneath any of these GOROOT/GOPATH directories: %s",
156			filepath.Dir(absFile), strings.Join(buildContext.SrcDirs(), ", "))
157	}
158	return
159}
160
161func segments(path string) []string {
162	return strings.Split(path, string(os.PathSeparator))
163}
164
165// prefixLen returns the length of the remainder of y if x is a prefix
166// of y, a negative number otherwise.
167func prefixLen(x, y []string) int {
168	d := len(y) - len(x)
169	if d >= 0 {
170		for i := range x {
171			if y[i] != x[i] {
172				return -1 // not a prefix
173			}
174		}
175	}
176	return d
177}
178
179type whatResult struct {
180	path       []ast.Node
181	modes      []string
182	srcdir     string
183	importPath string
184}
185
186func (r *whatResult) display(printf printfFunc) {
187	for _, n := range r.path {
188		printf(n, "%s", astutil.NodeDescription(n))
189	}
190	printf(nil, "modes: %s", r.modes)
191	printf(nil, "srcdir: %s", r.srcdir)
192	printf(nil, "import path: %s", r.importPath)
193}
194
195func (r *whatResult) toSerial(res *serial.Result, fset *token.FileSet) {
196	var enclosing []serial.SyntaxNode
197	for _, n := range r.path {
198		enclosing = append(enclosing, serial.SyntaxNode{
199			Description: astutil.NodeDescription(n),
200			Start:       fset.Position(n.Pos()).Offset,
201			End:         fset.Position(n.End()).Offset,
202		})
203	}
204	res.What = &serial.What{
205		Modes:      r.modes,
206		SrcDir:     r.srcdir,
207		ImportPath: r.importPath,
208		Enclosing:  enclosing,
209	}
210}
211