symbols.go (6673B)
1 // Copyright 2024 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 modindex 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/parser" 11 "go/token" 12 "go/types" 13 "iter" 14 "os" 15 "path/filepath" 16 "runtime" 17 "slices" 18 "strings" 19 "sync" 20 21 "golang.org/x/sync/errgroup" 22 ) 23 24 // The name of a symbol contains information about the symbol: 25 // <name> T for types, TD if the type is deprecated 26 // <name> C for consts, CD if the const is deprecated 27 // <name> V for vars, VD if the var is deprecated 28 // and for funcs: <name> F <num of return values> (<arg-name> <arg-type>)* 29 // any spaces in <arg-type> are replaced by $s so that the fields 30 // of the name are space separated. F is replaced by FD if the func 31 // is deprecated. 32 type symbol struct { 33 pkg string // name of the symbols's package 34 name string // declared name 35 kind string // T, C, V, or F, followed by D if deprecated 36 sig string // signature information, for F 37 } 38 39 // extractSymbols returns a (new, unordered) array of Entries, one for 40 // each provided package directory, describing its exported symbols. 41 func extractSymbols(cwd string, dirs iter.Seq[directory]) []Entry { 42 var ( 43 mu sync.Mutex 44 entries []Entry 45 ) 46 47 var g errgroup.Group 48 g.SetLimit(max(2, runtime.GOMAXPROCS(0)/2)) 49 for dir := range dirs { 50 g.Go(func() error { 51 thedir := filepath.Join(cwd, string(dir.path)) 52 mode := parser.SkipObjectResolution | parser.ParseComments 53 54 // Parse all Go files in dir and extract symbols. 55 dirents, err := os.ReadDir(thedir) 56 if err != nil { 57 return nil // log this someday? 58 } 59 var syms []symbol 60 for _, dirent := range dirents { 61 if !strings.HasSuffix(dirent.Name(), ".go") || 62 strings.HasSuffix(dirent.Name(), "_test.go") { 63 continue 64 } 65 fname := filepath.Join(thedir, dirent.Name()) 66 tr, err := parser.ParseFile(token.NewFileSet(), fname, nil, mode) 67 if err != nil { 68 continue // ignore errors, someday log them? 69 } 70 syms = append(syms, getFileExports(tr)...) 71 } 72 73 // Create an entry for the package. 74 pkg, names := processSyms(syms) 75 if pkg != "" { 76 mu.Lock() 77 defer mu.Unlock() 78 entries = append(entries, Entry{ 79 PkgName: pkg, 80 Dir: dir.path, 81 ImportPath: dir.importPath, 82 Version: dir.version, 83 Names: names, 84 }) 85 } 86 87 return nil 88 }) 89 } 90 g.Wait() // ignore error 91 92 return entries 93 } 94 95 func getFileExports(f *ast.File) []symbol { 96 pkg := f.Name.Name 97 if pkg == "main" || pkg == "" { 98 return nil 99 } 100 var ans []symbol 101 // should we look for //go:build ignore? 102 for _, decl := range f.Decls { 103 switch decl := decl.(type) { 104 case *ast.FuncDecl: 105 if decl.Recv != nil { 106 // ignore methods, as we are completing package selections 107 continue 108 } 109 name := decl.Name.Name 110 dtype := decl.Type 111 // not looking at dtype.TypeParams. That is, treating 112 // generic functions just like non-generic ones. 113 sig := dtype.Params 114 kind := "F" 115 if isDeprecated(decl.Doc) { 116 kind += "D" 117 } 118 result := []string{fmt.Sprintf("%d", dtype.Results.NumFields())} 119 for _, x := range sig.List { 120 // This code creates a string representing the type. 121 // TODO(pjw): it may be fragile: 122 // 1. x.Type could be nil, perhaps in ill-formed code 123 // 2. ExprString might someday change incompatibly to 124 // include struct tags, which can be arbitrary strings 125 if x.Type == nil { 126 // Can this happen without a parse error? (Files with parse 127 // errors are ignored in getSymbols) 128 continue // maybe report this someday 129 } 130 tp := types.ExprString(x.Type) 131 if len(tp) == 0 { 132 // Can this happen? 133 continue // maybe report this someday 134 } 135 // This is only safe if ExprString never returns anything with a $ 136 // The only place a $ can occur seems to be in a struct tag, which 137 // can be an arbitrary string literal, and ExprString does not presently 138 // print struct tags. So for this to happen the type of a formal parameter 139 // has to be a explicit struct, e.g. foo(x struct{a int "$"}) and ExprString 140 // would have to show the struct tag. Even testing for this case seems 141 // a waste of effort, but let's remember the possibility 142 if strings.Contains(tp, "$") { 143 continue 144 } 145 tp = strings.Replace(tp, " ", "$", -1) 146 if len(x.Names) == 0 { 147 result = append(result, "_") 148 result = append(result, tp) 149 } else { 150 for _, y := range x.Names { 151 result = append(result, y.Name) 152 result = append(result, tp) 153 } 154 } 155 } 156 sigs := strings.Join(result, " ") 157 if s := newsym(pkg, name, kind, sigs); s != nil { 158 ans = append(ans, *s) 159 } 160 case *ast.GenDecl: 161 depr := isDeprecated(decl.Doc) 162 switch decl.Tok { 163 case token.CONST, token.VAR: 164 tp := "V" 165 if decl.Tok == token.CONST { 166 tp = "C" 167 } 168 if depr { 169 tp += "D" 170 } 171 for _, sp := range decl.Specs { 172 for _, x := range sp.(*ast.ValueSpec).Names { 173 if s := newsym(pkg, x.Name, tp, ""); s != nil { 174 ans = append(ans, *s) 175 } 176 } 177 } 178 case token.TYPE: 179 tp := "T" 180 if depr { 181 tp += "D" 182 } 183 for _, sp := range decl.Specs { 184 if s := newsym(pkg, sp.(*ast.TypeSpec).Name.Name, tp, ""); s != nil { 185 ans = append(ans, *s) 186 } 187 } 188 } 189 } 190 } 191 return ans 192 } 193 194 func newsym(pkg, name, kind, sig string) *symbol { 195 if len(name) == 0 || !ast.IsExported(name) { 196 return nil 197 } 198 sym := symbol{pkg: pkg, name: name, kind: kind, sig: sig} 199 return &sym 200 } 201 202 func isDeprecated(doc *ast.CommentGroup) bool { 203 if doc == nil { 204 return false 205 } 206 // go.dev/wiki/Deprecated Paragraph starting 'Deprecated:' 207 // This code fails for /* Deprecated: */, but it's the code from 208 // gopls/internal/analysis/deprecated 209 for line := range strings.SplitSeq(doc.Text(), "\n\n") { 210 if strings.HasPrefix(line, "Deprecated:") { 211 return true 212 } 213 } 214 return false 215 } 216 217 // return the package name and the value for the symbols. 218 // if there are multiple packages, choose one arbitrarily 219 // the returned slice is sorted lexicographically 220 func processSyms(syms []symbol) (string, []string) { 221 if len(syms) == 0 { 222 return "", nil 223 } 224 slices.SortFunc(syms, func(l, r symbol) int { 225 return strings.Compare(l.name, r.name) 226 }) 227 pkg := syms[0].pkg 228 var names []string 229 for _, s := range syms { 230 if s.pkg != pkg { 231 // Symbols came from two files in same dir 232 // with different package declarations. 233 continue 234 } 235 var nx string 236 if s.sig != "" { 237 nx = fmt.Sprintf("%s %s %s", s.name, s.kind, s.sig) 238 } else { 239 nx = fmt.Sprintf("%s %s", s.name, s.kind) 240 } 241 names = append(names, nx) 242 } 243 return pkg, names 244 }