|  | // Copyright 2024 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package modindex | 
|  |  | 
|  | import ( | 
|  | "fmt" | 
|  | "log" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "regexp" | 
|  | "slices" | 
|  | "strings" | 
|  | "sync" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/mod/semver" | 
|  | "golang.org/x/tools/internal/gopathwalk" | 
|  | ) | 
|  |  | 
|  | type directory struct { | 
|  | path       Relpath | 
|  | importPath string | 
|  | version    string // semantic version | 
|  | syms       []symbol | 
|  | } | 
|  |  | 
|  | // filterDirs groups the directories by import path, | 
|  | // sorting the ones with the same import path by semantic version, | 
|  | // most recent first. | 
|  | func byImportPath(dirs []Relpath) (map[string][]*directory, error) { | 
|  | ans := make(map[string][]*directory) // key is import path | 
|  | for _, d := range dirs { | 
|  | ip, sv, err := DirToImportPathVersion(d) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | ans[ip] = append(ans[ip], &directory{ | 
|  | path:       d, | 
|  | importPath: ip, | 
|  | version:    sv, | 
|  | }) | 
|  | } | 
|  | for k, v := range ans { | 
|  | semanticSort(v) | 
|  | ans[k] = v | 
|  | } | 
|  | return ans, nil | 
|  | } | 
|  |  | 
|  | // sort the directories by semantic version, latest first | 
|  | func semanticSort(v []*directory) { | 
|  | slices.SortFunc(v, func(l, r *directory) int { | 
|  | if n := semver.Compare(l.version, r.version); n != 0 { | 
|  | return -n // latest first | 
|  | } | 
|  | return strings.Compare(string(l.path), string(r.path)) | 
|  | }) | 
|  | } | 
|  |  | 
|  | // modCacheRegexp splits a relpathpath into module, module version, and package. | 
|  | var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) | 
|  |  | 
|  | // DirToImportPathVersion computes import path and semantic version | 
|  | func DirToImportPathVersion(dir Relpath) (string, string, error) { | 
|  | m := modCacheRegexp.FindStringSubmatch(string(dir)) | 
|  | // m[1] is the module path | 
|  | // m[2] is the version major.minor.patch(-<pre release identifier) | 
|  | // m[3] is the rest of the package path | 
|  | if len(m) != 4 { | 
|  | return "", "", fmt.Errorf("bad dir %s", dir) | 
|  | } | 
|  | if !semver.IsValid(m[2]) { | 
|  | return "", "", fmt.Errorf("bad semantic version %s", m[2]) | 
|  | } | 
|  | // ToSlash is required for Windows. | 
|  | return filepath.ToSlash(m[1] + m[3]), m[2], nil | 
|  | } | 
|  |  | 
|  | // a region controls what directories to look at, for | 
|  | // updating the index incrementally, and for testing that. | 
|  | // (for testing one builds an index as of A, incrementally | 
|  | // updates it to B, and compares the result to an index build | 
|  | // as of B.) | 
|  | type region struct { | 
|  | onlyAfter, onlyBefore time.Time | 
|  | sync.Mutex | 
|  | ans []Relpath | 
|  | } | 
|  |  | 
|  | func findDirs(root string, onlyAfter, onlyBefore time.Time) []Relpath { | 
|  | roots := []gopathwalk.Root{{Path: root, Type: gopathwalk.RootModuleCache}} | 
|  | // TODO(PJW): adjust concurrency | 
|  | opts := gopathwalk.Options{ModulesEnabled: true, Concurrency: 1 /* ,Logf: log.Printf*/} | 
|  | betw := ®ion{ | 
|  | onlyAfter:  onlyAfter, | 
|  | onlyBefore: onlyBefore, | 
|  | } | 
|  | gopathwalk.WalkSkip(roots, betw.addDir, betw.skipDir, opts) | 
|  | return betw.ans | 
|  | } | 
|  |  | 
|  | func (r *region) addDir(rt gopathwalk.Root, dir string) { | 
|  | // do we need to check times? | 
|  | r.Lock() | 
|  | defer r.Unlock() | 
|  | x := filepath.ToSlash(string(toRelpath(Abspath(rt.Path), dir))) | 
|  | r.ans = append(r.ans, toRelpath(Abspath(rt.Path), x)) | 
|  | } | 
|  |  | 
|  | func (r *region) skipDir(_ gopathwalk.Root, dir string) bool { | 
|  | // The cache directory is already ignored in gopathwalk\ | 
|  | if filepath.Base(dir) == "internal" { | 
|  | return true | 
|  | } | 
|  | if strings.Contains(dir, "toolchain@") { | 
|  | return true | 
|  | } | 
|  | // don't look inside @ directories that are too old | 
|  | if strings.Contains(filepath.Base(dir), "@") { | 
|  | st, err := os.Stat(dir) | 
|  | if err != nil { | 
|  | log.Printf("can't stat dir %s %v", dir, err) | 
|  | return true | 
|  | } | 
|  | if st.ModTime().Before(r.onlyAfter) { | 
|  | return true | 
|  | } | 
|  | if st.ModTime().After(r.onlyBefore) { | 
|  | return true | 
|  | } | 
|  | } | 
|  | return false | 
|  | } |