// 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" "go/ast" "go/parser" "go/token" "go/types" "os" "path/filepath" "slices" "strings" "golang.org/x/sync/errgroup" ) // The name of a symbol contains information about the symbol: // T for types // C for consts // V for vars // and for funcs: F ( )* // any spaces in are replaced by $s so that the fields // of the name are space separated type symbol struct { pkg string // name of the symbols's package name string // declared name kind string // T, C, V, or F sig string // signature information, for F } // find the symbols for the best directories func getSymbols(cd Abspath, dirs map[string][]*directory) { var g errgroup.Group g.SetLimit(-1) // maybe throttle this some day for _, vv := range dirs { // throttling some day? d := vv[0] g.Go(func() error { thedir := filepath.Join(string(cd), string(d.path)) mode := parser.SkipObjectResolution fi, err := os.ReadDir(thedir) if err != nil { return nil // log this someday? } for _, fx := range fi { if !strings.HasSuffix(fx.Name(), ".go") || strings.HasSuffix(fx.Name(), "_test.go") { continue } fname := filepath.Join(thedir, fx.Name()) tr, err := parser.ParseFile(token.NewFileSet(), fname, nil, mode) if err != nil { continue // ignore errors, someday log them? } d.syms = append(d.syms, getFileExports(tr)...) } return nil }) } g.Wait() } func getFileExports(f *ast.File) []symbol { pkg := f.Name.Name if pkg == "main" { return nil } var ans []symbol // should we look for //go:build ignore? for _, decl := range f.Decls { switch decl := decl.(type) { case *ast.FuncDecl: if decl.Recv != nil { // ignore methods, as we are completing package selections continue } name := decl.Name.Name dtype := decl.Type // not looking at dtype.TypeParams. That is, treating // generic functions just like non-generic ones. sig := dtype.Params kind := "F" result := []string{fmt.Sprintf("%d", dtype.Results.NumFields())} for _, x := range sig.List { // This code creates a string representing the type. // TODO(pjw): it may be fragile: // 1. x.Type could be nil, perhaps in ill-formed code // 2. ExprString might someday change incompatibly to // include struct tags, which can be arbitrary strings if x.Type == nil { // Can this happen without a parse error? (Files with parse // errors are ignored in getSymbols) continue // maybe report this someday } tp := types.ExprString(x.Type) if len(tp) == 0 { // Can this happen? continue // maybe report this someday } // This is only safe if ExprString never returns anything with a $ // The only place a $ can occur seems to be in a struct tag, which // can be an arbitrary string literal, and ExprString does not presently // print struct tags. So for this to happen the type of a formal parameter // has to be a explict struct, e.g. foo(x struct{a int "$"}) and ExprString // would have to show the struct tag. Even testing for this case seems // a waste of effort, but let's not ignore such pathologies if strings.Contains(tp, "$") { continue } tp = strings.Replace(tp, " ", "$", -1) if len(x.Names) == 0 { result = append(result, "_") result = append(result, tp) } else { for _, y := range x.Names { result = append(result, y.Name) result = append(result, tp) } } } sigs := strings.Join(result, " ") if s := newsym(pkg, name, kind, sigs); s != nil { ans = append(ans, *s) } case *ast.GenDecl: switch decl.Tok { case token.CONST, token.VAR: tp := "V" if decl.Tok == token.CONST { tp = "C" } for _, sp := range decl.Specs { for _, x := range sp.(*ast.ValueSpec).Names { if s := newsym(pkg, x.Name, tp, ""); s != nil { ans = append(ans, *s) } } } case token.TYPE: for _, sp := range decl.Specs { if s := newsym(pkg, sp.(*ast.TypeSpec).Name.Name, "T", ""); s != nil { ans = append(ans, *s) } } } } } return ans } func newsym(pkg, name, kind, sig string) *symbol { if len(name) == 0 || !ast.IsExported(name) { return nil } sym := symbol{pkg: pkg, name: name, kind: kind, sig: sig} return &sym } // return the package name and the value for the symbols. // if there are multiple packages, choose one arbitrarily // the returned slice is sorted lexicographically func processSyms(syms []symbol) (string, []string) { if len(syms) == 0 { return "", nil } slices.SortFunc(syms, func(l, r symbol) int { return strings.Compare(l.name, r.name) }) pkg := syms[0].pkg var names []string for _, s := range syms { var nx string if s.pkg == pkg { if s.sig != "" { nx = fmt.Sprintf("%s %s %s", s.name, s.kind, s.sig) } else { nx = fmt.Sprintf("%s %s", s.name, s.kind) } names = append(names, nx) } else { continue // PJW: do we want to keep track of these? } } return pkg, names }