cuelang.org/go@v0.13.0/internal/golangorgx/gopls/cache/load.go (about) 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 5 package cache 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "maps" 13 "path/filepath" 14 "sort" 15 16 "cuelang.org/go/cue/build" 17 "cuelang.org/go/cue/load" 18 "cuelang.org/go/internal/golangorgx/gopls/cache/metadata" 19 "cuelang.org/go/internal/golangorgx/gopls/file" 20 "cuelang.org/go/internal/golangorgx/gopls/protocol" 21 "cuelang.org/go/internal/golangorgx/gopls/util/immutable" 22 "cuelang.org/go/internal/golangorgx/gopls/util/pathutil" 23 "cuelang.org/go/internal/golangorgx/tools/event" 24 "golang.org/x/tools/go/packages" 25 ) 26 27 var loadID uint64 // atomic identifier for loads 28 29 // errNoInstances indicates that a load query returned no instances. 30 var errNoInstances = errors.New("no instances returned") 31 32 // load calls packages.Load for the given scopes, updating package metadata, 33 // import graph, and mapped files with the result. 34 // 35 // The resulting error may wrap the moduleErrorMap error type, representing 36 // errors associated with specific modules. 37 // 38 // If scopes contains a file scope there must be exactly one scope. 39 func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) { 40 //id := atomic.AddUint64(&loadID, 1) 41 //eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging 42 43 ctx, done := event.Start(ctx, "cache.snapshot.load") 44 defer done() 45 46 var insts []*build.Instance 47 48 overlays := s.buildOverlay() 49 50 for _, scope := range scopes { 51 switch scope := scope.(type) { 52 case fileLoadScope: 53 uri := protocol.DocumentURI(scope) 54 fh := s.FindFile(uri) 55 if fh == nil || s.FileKind(fh) != file.CUE { 56 // Don't try to load a file that doesn't exist, or isn't a go file. 57 continue 58 } 59 _, err := fh.Content() 60 if err != nil { 61 continue 62 } 63 cfg := &load.Config{ 64 Dir: filepath.Dir(uri.Path()), 65 Overlay: overlays, 66 } 67 // We use ./... because we want to load all instances that 68 // involve this particular file. This will include instances 69 // from child directories that use the same package as our 70 // file. 71 // 72 // TODO(ms): work out whether we can use the cfg.Package 73 // field to limit us to a single package, and thus do less 74 // work. 75 insts = append(insts, load.Instances([]string{"./..."}, cfg)...) 76 77 case packageLoadScope: 78 cfg := &load.Config{ 79 Dir: s.Folder().Path(), 80 Overlay: overlays, 81 } 82 insts = append(insts, load.Instances([]string{string(scope)}, cfg)...) 83 84 case moduleLoadScope: 85 cfg := &load.Config{ 86 Module: scope.modulePath, 87 Package: "*", 88 Dir: scope.dir, 89 Overlay: overlays, 90 } 91 insts = append(insts, load.Instances([]string{"./..."}, cfg)...) 92 93 case viewLoadScope: 94 // We're loading the workspace 95 cfg := &load.Config{ 96 Package: "*", 97 Dir: s.Folder().Path(), 98 Overlay: overlays, 99 } 100 // TODO(ms): In gopls, if the view is adhoc mode, here they 101 // only load the directory and not ./... Why?! 102 insts = append(insts, load.Instances([]string{"./..."}, cfg)...) 103 104 default: 105 panic(fmt.Sprintf("unknown scope type %T", scope)) 106 } 107 } 108 109 if len(insts) == 0 { 110 return errNoInstances 111 } 112 113 byImportPath := make(map[ImportPath]*build.Instance) 114 for _, inst := range insts { 115 buildMetadata(byImportPath, inst) 116 } 117 118 s.mu.Lock() 119 120 var files []protocol.DocumentURI // files to preload 121 seenFiles := make(map[protocol.DocumentURI]struct{}) 122 updates := maps.Clone(byImportPath) 123 for path, inst := range byImportPath { 124 if _, exists := s.meta.Packages[metadata.ImportPath(inst.ImportPath)]; exists { 125 delete(updates, path) 126 continue 127 } 128 for _, path := range inst.BuildFiles { 129 uri := protocol.URIFromPath(path.Filename) 130 if _, seen := seenFiles[uri]; seen { 131 continue 132 } 133 seenFiles[uri] = struct{}{} 134 files = append(files, uri) 135 } 136 s.shouldLoad.Delete(ImportPath(inst.ImportPath)) 137 } 138 139 s.meta = s.meta.Update(updates) 140 141 s.mu.Unlock() 142 143 s.preloadFiles(ctx, files) 144 145 return nil 146 } 147 148 func buildMetadata(byImportPath map[ImportPath]*build.Instance, inst *build.Instance) { 149 // Note that in cue, it is not considered that a child package 150 // "imports" an ancestor package with the same package 151 // name. E.g. cue/load.Instances of an importPath "foo/bar:x" will 152 // automatically find and include "foo:x" if it exists. Further, we 153 // will find the cue files from the parent directory in the 154 // child-instance's BuildFiles. But it is not the case that the 155 // parent package will appear in the child's Imports. 156 importPath := ImportPath(inst.ImportPath) 157 if _, seen := byImportPath[importPath]; seen { 158 // think: diamond imports - A imports B, A imports C. B imports 159 // D, C imports D. We'll see D twice then. 160 return 161 } 162 byImportPath[importPath] = inst 163 for _, inst := range inst.Imports { 164 buildMetadata(byImportPath, inst) 165 } 166 } 167 168 type moduleErrorMap struct { 169 errs map[string][]packages.Error // module path -> errors 170 } 171 172 func (m *moduleErrorMap) Error() string { 173 var paths []string // sort for stability 174 for path, errs := range m.errs { 175 if len(errs) > 0 { // should always be true, but be cautious 176 paths = append(paths, path) 177 } 178 } 179 sort.Strings(paths) 180 181 var buf bytes.Buffer 182 fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths)) 183 for _, path := range paths { 184 fmt.Fprintf(&buf, "\t%s:%s\n", path, m.errs[path][0].Msg) 185 } 186 187 return buf.String() 188 } 189 190 // isWorkspacePackageLocked reports whether p is a workspace package for the 191 // snapshot s. 192 // 193 // Workspace packages are packages that we consider the user to be actively 194 // working on. As such, they are re-diagnosed on every keystroke, and searched 195 // for various workspace-wide queries such as references or workspace symbols. 196 // 197 // See the commentary inline for a description of the workspace package 198 // heuristics. 199 // 200 // s.mu must be held while calling this function. 201 func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, inst *build.Instance) bool { 202 if metadata.IsCommandLineArguments(metadata.ImportPath(inst.ImportPath)) { 203 // TODO(ms) original is more complex; decide what we're doing about commandlineargs in general. 204 return false 205 } 206 207 // Apply filtering logic. 208 // 209 // Workspace packages must contain at least one non-filtered file. 210 filterFunc := s.view.filterFunc() 211 uris := make(map[protocol.DocumentURI]unit) // filtered package URIs 212 for _, file := range inst.BuildFiles { 213 uri := protocol.URIFromPath(file.Filename) 214 if !filterFunc(uri) { 215 uris[uri] = struct{}{} 216 } 217 } 218 if len(uris) == 0 { 219 return false // no non-filtered files 220 } 221 222 // For non-module views (of type GOPATH or AdHoc), or if 223 // expandWorkspaceToModule is unset, workspace packages must be contained in 224 // the workspace folder. 225 // 226 // For module views (of type GoMod or GoWork), packages must in any case be 227 // in a workspace module (enforced below). 228 if !s.view.moduleMode() || !s.Options().ExpandWorkspaceToModule { 229 folder := s.view.folder.Dir.Path() 230 inFolder := false 231 for uri := range uris { 232 if pathutil.InDir(folder, uri.Path()) { 233 inFolder = true 234 break 235 } 236 } 237 if !inFolder { 238 return false 239 } 240 } 241 242 // In module mode, a workspace package must be contained in a workspace 243 // module. 244 if s.view.moduleMode() { 245 if inst.Module == "" { 246 return false 247 } 248 modURI := protocol.URIFromPath(inst.Root) 249 _, ok := s.view.workspaceModFiles[modURI] 250 return ok 251 } 252 253 return true // an ad-hoc package or GOPATH package 254 } 255 256 // containsOpenFileLocked reports whether any file referenced by inst 257 // is open in the snapshot s. 258 // 259 // s.mu must be held while calling this function. 260 func containsOpenFileLocked(s *Snapshot, inst *build.Instance) bool { 261 for _, file := range inst.BuildFiles { 262 fh, _ := s.files.get(protocol.URIFromPath(file.Filename)) 263 if _, open := fh.(*overlay); open { 264 return true 265 } 266 } 267 return false 268 } 269 270 // computeWorkspacePackagesLocked computes workspace packages in the 271 // snapshot s for the given metadata graph. The result does not 272 // contain intermediate test variants. 273 // 274 // s.mu must be held while calling this function. 275 func computeWorkspacePackagesLocked(s *Snapshot, meta *metadata.Graph) immutable.Map[ImportPath, unit] { 276 workspacePackages := make(map[ImportPath]unit) 277 for _, inst := range meta.Packages { 278 if !isWorkspacePackageLocked(s, meta, inst) { 279 continue 280 } 281 282 workspacePackages[ImportPath(inst.ImportPath)] = unit{} 283 } 284 return immutable.MapOf(workspacePackages) 285 } 286 287 // allFilesHaveRealPackages reports whether all files referenced by inst are 288 // contained in a "real" package (not command-line-arguments). 289 // 290 // If inst is valid but all "real" packages containing any file are invalid, this 291 // function returns false. 292 // 293 // If inst is not a command-line-arguments package, this is trivially true. 294 func allFilesHaveRealPackages(g *metadata.Graph, inst *build.Instance) bool { 295 checkURIs: 296 for _, file := range inst.BuildFiles { 297 for _, pkgPath := range g.FilesToPackage[protocol.URIFromPath(file.Filename)] { 298 if !metadata.IsCommandLineArguments(pkgPath) { 299 continue checkURIs 300 } 301 } 302 return false 303 } 304 return true 305 }