This repository has been archived by the owner on Sep 26, 2023. It is now read-only.
/
dependencies.go
322 lines (265 loc) · 10.1 KB
/
dependencies.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package gomod
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/url"
"path"
"regexp"
"runtime"
"strings"
"sync"
"github.com/sourcegraph/lsif-go/internal/command"
"github.com/sourcegraph/lsif-go/internal/output"
"golang.org/x/tools/go/vcs"
)
type GoModule struct {
Name string
Version string
}
// ListDependencies returns a map from dependency import paths to the imported module's name
// and version as declared by the go.mod file in the current directory. The given root module
// and version are used to resolve replace directives with local file paths. The root module
// is expected to be a resolved import path (a valid URL, including a scheme).
func ListDependencies(dir, rootModule, rootVersion string, outputOptions output.Options) (dependencies map[string]GoModule, err error) {
if !isModule(dir) {
log.Println("WARNING: No go.mod file found in current directory.")
return nil, nil
}
resolve := func() {
var output, modOutput string
output, err = command.Run(dir, "go", "list", "-mod=readonly", "-m", "-json", "all")
if err != nil {
err = fmt.Errorf("failed to list modules: %v\n%s", err, output)
return
}
// The reason we run this command separate is because we want the
// information about this package specifically. Currently, it seems
// that "go list all" will place the current modules information first
// in the list, but we don't know that that is guaranteed.
//
// Because of that, we do a separate execution to guarantee we get only
// this package information to use to determine the corresponding
// goVersion.
modOutput, err = command.Run(dir, "go", "list", "-mod=readonly", "-m", "-json")
if err != nil {
err = fmt.Errorf("failed to list module info: %v\n%s", err, output)
return
}
dependencies, err = parseGoListOutput(output, modOutput, rootVersion)
if err != nil {
return
}
modules := make([]string, 0, len(dependencies))
for _, module := range dependencies {
modules = append(modules, module.Name)
}
resolvedImportPaths := resolveImportPaths(rootModule, modules)
mapImportPaths(dependencies, resolvedImportPaths)
}
output.WithProgress("Listing dependencies", resolve, outputOptions)
return dependencies, err
}
// listProjectDependencies finds any packages from "$ go list all" that are NOT declared
// as part of the current project.
//
// NOTE: This is different from the other dependencies stored in the indexer because it
// does not modules, but packages.
func ListProjectDependencies(projectRoot string) ([]string, error) {
projectPackageOutput, err := command.Run(projectRoot, "go", "list", "./...")
if err != nil {
return nil, fmt.Errorf("failed to list project packages: %v\n%s", err, projectPackageOutput)
}
projectPackages := map[string]struct{}{}
for _, pkg := range strings.Split(projectPackageOutput, "\n") {
projectPackages[pkg] = struct{}{}
}
output, err := command.Run(projectRoot, "go", "list", "all")
if err != nil {
return nil, fmt.Errorf("failed to list dependency packages: %v\n%s", err, output)
}
dependencyPackages := []string{"std"}
for _, dep := range strings.Split(output, "\n") {
// It's a dependency if it's not in the projectPackages
if _, ok := projectPackages[dep]; !ok {
dependencyPackages = append(dependencyPackages, dep)
}
}
return dependencyPackages, nil
}
type jsonModule struct {
Name string `json:"Path"`
Version string `json:"Version"`
Replace *jsonModule `json:"Replace"`
// The Golang version required for this module
GoVersion string `json:"GoVersion"`
}
// parseGoListOutput parse the JSON output of `go list -m`. This method returns a map from
// import paths to pairs of declared (unresolved) module names and version pairs that respect
// replacement directives specified in go.mod. Replace directives indicating a local file path
// will create a module with the given root version, which is expected to be the same version
// as the module being indexed.
func parseGoListOutput(output, modOutput, rootVersion string) (map[string]GoModule, error) {
dependencies := map[string]GoModule{}
decoder := json.NewDecoder(strings.NewReader(output))
for {
var module jsonModule
if err := decoder.Decode(&module); err != nil {
if err == io.EOF {
break
}
return nil, err
}
// Stash original name before applying replacement
importPath := module.Name
// If there's a replace directive, use that module instead
if module.Replace != nil {
module = *module.Replace
}
// Local file paths and root modules
if module.Version == "" {
module.Version = rootVersion
}
dependencies[importPath] = GoModule{
Name: module.Name,
Version: cleanVersion(module.Version),
}
}
var thisModule jsonModule
if err := json.NewDecoder(strings.NewReader(modOutput)).Decode(&thisModule); err != nil {
return nil, err
}
if thisModule.GoVersion == "" {
return nil, errors.New("could not find GoVersion for current module")
}
setGolangDependency(dependencies, thisModule.GoVersion)
return dependencies, nil
}
// The repository to find the source code for golang.
var golangRepository = "github.com/golang/go"
func setGolangDependency(dependencies map[string]GoModule, goVersion string) {
dependencies[golangRepository] = GoModule{
Name: golangRepository,
// The reason we prefix version with "go" is because in golang/go, all the release
// tags are prefixed with "go". So turn "1.15" -> "go1.15"
Version: fmt.Sprintf("go%s", goVersion),
}
}
func GetGolangDependency(dependencies map[string]GoModule) GoModule {
return dependencies[golangRepository]
}
// NormalizeMonikerPackage returns a normalized path to ensure that all
// standard library paths are handled the same. Primarily to make sure
// that both the golangRepository and "std/" paths are normalized.
func NormalizeMonikerPackage(path string) string {
// When indexing _within_ the golang/go repository, `std/` is prefixed
// to packages. So we trim that here just to be sure that we keep
// consistent names.
normalizedPath := strings.TrimPrefix(path, "std/")
if !isStandardlibPackge(normalizedPath) {
return path
}
// Make sure we don't see double "std/" in the package for the moniker
return fmt.Sprintf("%s/std/%s", golangRepository, normalizedPath)
}
// versionPattern matches a versioning ending in a 12-digit sha, e.g., vX.Y.Z.-yyyymmddhhmmss-abcdefabcdef
var versionPattern = regexp.MustCompile(`^.*-([a-f0-9]{12})$`)
// cleanVersion normalizes a module version string.
func cleanVersion(version string) string {
version = strings.TrimSpace(strings.TrimSuffix(version, "// indirect"))
version = strings.TrimSpace(strings.TrimSuffix(version, "+incompatible"))
if matches := versionPattern.FindStringSubmatch(version); len(matches) > 0 {
return matches[1]
}
return version
}
// resolveImportPaths returns a map of import paths to resolved code host and path
// suffix usable for moniker identifiers. The given root module is used to resolve
// replace directives with local file paths and is expected to be a resolved import
// path (a valid URL, including a scheme).
func resolveImportPaths(rootModule string, modules []string) map[string]string {
ch := make(chan string, len(modules))
for _, module := range modules {
ch <- module
}
close(ch)
var m sync.Mutex
namesToResolve := map[string]string{}
var wg sync.WaitGroup
for i := 0; i < runtime.GOMAXPROCS(0); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for name := range ch {
// Stash original name before applying replacement
originalName := name
// Try to resolve the import path if it looks like a local path
name, err := resolveLocalPath(name, rootModule)
if err != nil {
log.Println(fmt.Sprintf("WARNING: Failed to resolve local %s (%s).", name, err))
continue
}
// Determine path suffix relative to the import path
resolved, ok := resolveRepoRootForImportPath(name)
if !ok {
continue
}
m.Lock()
namesToResolve[originalName] = resolved
m.Unlock()
}
}()
}
wg.Wait()
return namesToResolve
}
// resolveRepoRootForImportPath will get the resolved name after handling vsc RepoRoots and any
// necessary handling of the standard library
func resolveRepoRootForImportPath(name string) (string, bool) {
// When indexing golang/go, there are some references to the package "std" itself.
// Generally, "std/" is not referenced directly (it is just assumed when you have "fmt" or similar
// in your imports), but inside of golang/go, it is directly referenced.
//
// In that case, we just return it directly, there is no other resolving to do.
if name == "std" {
return name, true
}
repoRoot, err := vcs.RepoRootForImportPath(name, false)
if err != nil {
log.Println(fmt.Sprintf("WARNING: Failed to resolve repo %s (%s) %s.", name, err, repoRoot))
return "", false
}
suffix := strings.TrimPrefix(name, repoRoot.Root)
return repoRoot.Repo + suffix, true
}
// resolveLocalPath converts the given name to an import path if it looks like a local path based on
// the given root module. The root module, if non-empty, is expected to be a resolved import path
// (a valid URL, including a scheme). If the name does not look like a local path, it will be returned
// unchanged.
func resolveLocalPath(name, rootModule string) (string, error) {
if rootModule == "" || !strings.HasPrefix(name, ".") {
return name, nil
}
parsedRootModule, err := url.Parse(rootModule)
if err != nil {
return "", err
}
// Join path relative to the root to the parsed module
parsedRootModule.Path = path.Join(parsedRootModule.Path, name)
// Remove scheme so it's resolvable again as an import path
return strings.TrimPrefix(parsedRootModule.String(), parsedRootModule.Scheme+"://"), nil
}
// mapImportPaths replace each module name with the value in the given resolved import paths
// map. If the module name is not present in the map, no change is made to the module value.
func mapImportPaths(dependencies map[string]GoModule, resolvedImportPaths map[string]string) {
for importPath, module := range dependencies {
if name, ok := resolvedImportPaths[module.Name]; ok {
dependencies[importPath] = GoModule{
Name: name,
Version: module.Version,
}
}
}
}