1// Copyright 2019 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 cache
6
7import (
8	"context"
9	"fmt"
10	"go/types"
11
12	"golang.org/x/tools/go/packages"
13	"golang.org/x/tools/internal/lsp/source"
14	"golang.org/x/tools/internal/lsp/telemetry"
15	"golang.org/x/tools/internal/span"
16	"golang.org/x/tools/internal/telemetry/log"
17	"golang.org/x/tools/internal/telemetry/tag"
18	"golang.org/x/tools/internal/telemetry/trace"
19	errors "golang.org/x/xerrors"
20)
21
22type metadata struct {
23	id              packageID
24	pkgPath         packagePath
25	name            string
26	goFiles         []span.URI
27	compiledGoFiles []span.URI
28	typesSizes      types.Sizes
29	errors          []packages.Error
30	deps            []packageID
31	missingDeps     map[packagePath]struct{}
32
33	// config is the *packages.Config associated with the loaded package.
34	config *packages.Config
35}
36
37func (s *snapshot) load(ctx context.Context, scope source.Scope) ([]*metadata, error) {
38	uri := scope.URI()
39	var query string
40	switch scope.(type) {
41	case source.FileURI:
42		query = fmt.Sprintf("file=%s", scope.URI().Filename())
43	case source.DirectoryURI:
44		query = fmt.Sprintf("%s/...", scope.URI().Filename())
45		// Simplify the query if it will be run in the requested directory.
46		// This ensures compatibility with Go 1.12 that doesn't allow
47		// <directory>/... in GOPATH mode.
48		if s.view.folder.Filename() == scope.URI().Filename() {
49			query = "./..."
50		}
51	default:
52		panic(fmt.Errorf("unsupported scope type %T", scope))
53	}
54	ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.URI.Of(uri))
55	defer done()
56
57	cfg := s.view.Config(ctx)
58	pkgs, err := packages.Load(cfg, query)
59
60	// If the context was canceled, return early.
61	// Otherwise, we might be type-checking an incomplete result.
62	if err == context.Canceled {
63		return nil, errors.Errorf("no metadata for %s: %v", uri, err)
64	}
65	log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
66	if len(pkgs) == 0 {
67		if err == nil {
68			err = errors.Errorf("no packages found for query %s", query)
69		}
70	}
71	if err != nil {
72		return nil, err
73	}
74	return s.updateMetadata(ctx, scope, pkgs, cfg)
75}
76
77// shouldLoad reparses a file's package and import declarations to
78// determine if they have changed.
79func (c *cache) shouldLoad(ctx context.Context, s *snapshot, originalFH, currentFH source.FileHandle) bool {
80	if originalFH == nil {
81		return true
82	}
83	// If the file is a mod file, we should always load.
84	if originalFH.Identity().Kind == currentFH.Identity().Kind && currentFH.Identity().Kind == source.Mod {
85		return true
86	}
87
88	// Get the original and current parsed files in order to check package name and imports.
89	original, _, _, originalErr := c.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx)
90	current, _, _, currentErr := c.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx)
91	if originalErr != nil || currentErr != nil {
92		return (originalErr == nil) != (currentErr == nil)
93	}
94
95	// Check if the package's metadata has changed. The cases handled are:
96	//
97	//    1. A package's name has changed
98	//    2. A file's imports have changed
99	//
100	if original.Name.Name != current.Name.Name {
101		return true
102	}
103	// If the package's imports have increased, definitely re-run `go list`.
104	if len(original.Imports) < len(current.Imports) {
105		return true
106	}
107	importSet := make(map[string]struct{})
108	for _, importSpec := range original.Imports {
109		importSet[importSpec.Path.Value] = struct{}{}
110	}
111	// If any of the current imports were not in the original imports.
112	for _, importSpec := range current.Imports {
113		if _, ok := importSet[importSpec.Path.Value]; !ok {
114			return true
115		}
116	}
117	return false
118}
119
120func (s *snapshot) updateMetadata(ctx context.Context, uri source.Scope, pkgs []*packages.Package, cfg *packages.Config) ([]*metadata, error) {
121	var results []*metadata
122	for _, pkg := range pkgs {
123		log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
124
125		// Set the metadata for this package.
126		if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg, map[packageID]struct{}{}); err != nil {
127			return nil, err
128		}
129		m := s.getMetadata(packageID(pkg.ID))
130		if m != nil {
131			results = append(results, m)
132		}
133	}
134
135	// Rebuild the import graph when the metadata is updated.
136	s.clearAndRebuildImportGraph()
137
138	if len(results) == 0 {
139		return nil, errors.Errorf("no metadata for %s", uri)
140	}
141	return results, nil
142}
143
144func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config, seen map[packageID]struct{}) error {
145	id := packageID(pkg.ID)
146	if _, ok := seen[id]; ok {
147		return errors.Errorf("import cycle detected: %q", id)
148	}
149	// Recreate the metadata rather than reusing it to avoid locking.
150	m := &metadata{
151		id:         id,
152		pkgPath:    pkgPath,
153		name:       pkg.Name,
154		typesSizes: pkg.TypesSizes,
155		errors:     pkg.Errors,
156		config:     cfg,
157	}
158
159	for _, filename := range pkg.CompiledGoFiles {
160		uri := span.FileURI(filename)
161		m.compiledGoFiles = append(m.compiledGoFiles, uri)
162		s.addID(uri, m.id)
163	}
164	for _, filename := range pkg.GoFiles {
165		uri := span.FileURI(filename)
166		m.goFiles = append(m.goFiles, uri)
167		s.addID(uri, m.id)
168	}
169
170	seen[id] = struct{}{}
171	copied := make(map[packageID]struct{})
172	for k, v := range seen {
173		copied[k] = v
174	}
175	for importPath, importPkg := range pkg.Imports {
176		importPkgPath := packagePath(importPath)
177		importID := packageID(importPkg.ID)
178
179		m.deps = append(m.deps, importID)
180
181		// Don't remember any imports with significant errors.
182		if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
183			if m.missingDeps == nil {
184				m.missingDeps = make(map[packagePath]struct{})
185			}
186			m.missingDeps[importPkgPath] = struct{}{}
187			continue
188		}
189		dep := s.getMetadata(importID)
190		if dep == nil {
191			if err := s.updateImports(ctx, importPkgPath, importPkg, cfg, copied); err != nil {
192				log.Error(ctx, "error in dependency", err)
193			}
194		}
195	}
196
197	// Add the metadata to the cache.
198	s.setMetadata(m)
199
200	return nil
201}
202