1package parser
2
3import (
4	"bytes"
5	"fmt"
6	"go/build"
7	"io/ioutil"
8	"os"
9	"os/exec"
10	"path"
11	"path/filepath"
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 pkgPathFromGoModCache = struct {
94	paths map[string]string
95	sync.RWMutex
96}{
97	paths: make(map[string]string),
98}
99
100func getModulePath(goModPath string) string {
101	pkgPathFromGoModCache.RLock()
102	pkgPath, ok := pkgPathFromGoModCache.paths[goModPath]
103	pkgPathFromGoModCache.RUnlock()
104	if ok {
105		return pkgPath
106	}
107
108	defer func() {
109		pkgPathFromGoModCache.Lock()
110		pkgPathFromGoModCache.paths[goModPath] = pkgPath
111		pkgPathFromGoModCache.Unlock()
112	}()
113
114	data, err := ioutil.ReadFile(goModPath)
115	if err != nil {
116		return ""
117	}
118	pkgPath = modulePath(data)
119	return pkgPath
120}
121
122func getPkgPathFromGOPATH(fname string, isDir bool) (string, error) {
123	gopath := os.Getenv("GOPATH")
124	if gopath == "" {
125		gopath = build.Default.GOPATH
126	}
127
128	for _, p := range strings.Split(gopath, string(filepath.ListSeparator)) {
129		prefix := filepath.Join(p, "src") + string(filepath.Separator)
130		rel, err := filepath.Rel(prefix, fname)
131		if err == nil && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
132			if !isDir {
133				return path.Dir(filePathToPackagePath(rel)), nil
134			} else {
135				return path.Clean(filePathToPackagePath(rel)), nil
136			}
137		}
138	}
139
140	return "", fmt.Errorf("file '%v' is not in GOPATH '%v'", fname, gopath)
141}
142
143func filePathToPackagePath(path string) string {
144	return filepath.ToSlash(path)
145}
146