src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }