1package parser
2
3import (
4	"bytes"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"os/exec"
9	"path"
10	"path/filepath"
11	"strconv"
12	"strings"
13	"sync"
14)
15
16func getPkgPath(fname string, isDir bool) (string, error) {
17	if !filepath.IsAbs(fname) {
18		pwd, err := os.Getwd()
19		if err != nil {
20			return "", err
21		}
22		fname = filepath.Join(pwd, fname)
23	}
24
25	goModPath, _ := goModPath(fname, isDir)
26	if strings.Contains(goModPath, "go.mod") {
27		pkgPath, err := getPkgPathFromGoMod(fname, isDir, goModPath)
28		if err != nil {
29			return "", err
30		}
31
32		return pkgPath, nil
33	}
34
35	return getPkgPathFromGOPATH(fname, isDir)
36}
37
38var goModPathCache = struct {
39	paths map[string]string
40	sync.RWMutex
41}{
42	paths: make(map[string]string),
43}
44
45// empty if no go.mod, GO111MODULE=off or go without go modules support
46func goModPath(fname string, isDir bool) (string, error) {
47	root := fname
48	if !isDir {
49		root = filepath.Dir(fname)
50	}
51
52	goModPathCache.RLock()
53	goModPath, ok := goModPathCache.paths[root]
54	goModPathCache.RUnlock()
55	if ok {
56		return goModPath, nil
57	}
58
59	defer func() {
60		goModPathCache.Lock()
61		goModPathCache.paths[root] = goModPath
62		goModPathCache.Unlock()
63	}()
64
65	cmd := exec.Command("go", "env", "GOMOD")
66	cmd.Dir = root
67
68	stdout, err := cmd.Output()
69	if err != nil {
70		return "", err
71	}
72
73	goModPath = string(bytes.TrimSpace(stdout))
74
75	return goModPath, nil
76}
77
78func getPkgPathFromGoMod(fname string, isDir bool, goModPath string) (string, error) {
79	modulePath := getModulePath(goModPath)
80	if modulePath == "" {
81		return "", fmt.Errorf("cannot determine module path from %s", goModPath)
82	}
83
84	rel := path.Join(modulePath, filePathToPackagePath(strings.TrimPrefix(fname, filepath.Dir(goModPath))))
85
86	if !isDir {
87		return path.Dir(rel), nil
88	}
89
90	return path.Clean(rel), nil
91}
92
93var (
94	modulePrefix          = []byte("\nmodule ")
95	pkgPathFromGoModCache = make(map[string]string)
96)
97
98func getModulePath(goModPath string) string {
99	pkgPath, ok := pkgPathFromGoModCache[goModPath]
100	if ok {
101		return pkgPath
102	}
103
104	defer func() {
105		pkgPathFromGoModCache[goModPath] = pkgPath
106	}()
107
108	data, err := ioutil.ReadFile(goModPath)
109	if err != nil {
110		return ""
111	}
112	var i int
113	if bytes.HasPrefix(data, modulePrefix[1:]) {
114		i = 0
115	} else {
116		i = bytes.Index(data, modulePrefix)
117		if i < 0 {
118			return ""
119		}
120		i++
121	}
122	line := data[i:]
123
124	// Cut line at \n, drop trailing \r if present.
125	if j := bytes.IndexByte(line, '\n'); j >= 0 {
126		line = line[:j]
127	}
128	if line[len(line)-1] == '\r' {
129		line = line[:len(line)-1]
130	}
131	line = line[len("module "):]
132
133	// If quoted, unquote.
134	pkgPath = strings.TrimSpace(string(line))
135	if pkgPath != "" && pkgPath[0] == '"' {
136		s, err := strconv.Unquote(pkgPath)
137		if err != nil {
138			return ""
139		}
140		pkgPath = s
141	}
142	return pkgPath
143}
144
145func getPkgPathFromGOPATH(fname string, isDir bool) (string, error) {
146	gopath := os.Getenv("GOPATH")
147	if gopath == "" {
148		var err error
149		gopath, err = getDefaultGoPath()
150		if err != nil {
151			return "", fmt.Errorf("cannot determine GOPATH: %s", err)
152		}
153	}
154
155	for _, p := range strings.Split(gopath, string(filepath.ListSeparator)) {
156		prefix := filepath.Join(p, "src") + string(filepath.Separator)
157		if rel := strings.TrimPrefix(fname, prefix); rel != fname {
158			if !isDir {
159				return path.Dir(filePathToPackagePath(rel)), nil
160			} else {
161				return path.Clean(filePathToPackagePath(rel)), nil
162			}
163		}
164	}
165
166	return "", fmt.Errorf("file '%v' is not in GOPATH", fname)
167}
168
169func filePathToPackagePath(path string) string {
170	return filepath.ToSlash(path)
171}
172