| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761 |
- // Copyright 2022 The Gc Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- //go:generate stringer -output stringer.go -linecomment -type=Kind,ScopeKind,ChanDir,TypeCheck
- package gc // modernc.org/gc/v3
- import (
- "fmt"
- "go/build"
- "go/build/constraint"
- "go/token"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "runtime"
- "sort"
- "strconv"
- "strings"
- "sync"
- "unicode"
- "github.com/hashicorp/golang-lru/v2"
- )
- var (
- trcErrors bool
- )
- type FileFilter func(cfg *Config, importPath string, matchedFSPaths []string, withTestFiles bool) (pkgFiles []string, err error)
- type TypeCheck int
- const (
- TypeCheckNone TypeCheck = iota
- TypeCheckAll
- )
- type cacheKey struct {
- buildTagsKey string
- cfg *Config
- fsPath string
- goarch string
- goos string
- gopathKey string
- goroot string
- importPath string
- typeCheck TypeCheck
- withTestFiles bool
- }
- type cacheItem struct {
- pkg *Package
- ch chan struct{}
- }
- func newCacheItem() *cacheItem { return &cacheItem{ch: make(chan struct{})} }
- func (c *cacheItem) set(pkg *Package) {
- c.pkg = pkg
- close(c.ch)
- }
- func (c *cacheItem) wait() *Package {
- <-c.ch
- return c.pkg
- }
- type Cache struct {
- sync.Mutex
- lru *lru.TwoQueueCache[cacheKey, *cacheItem]
- }
- func NewCache(size int) (*Cache, error) {
- c, err := lru.New2Q[cacheKey, *cacheItem](size)
- if err != nil {
- return nil, err
- }
- return &Cache{lru: c}, nil
- }
- func MustNewCache(size int) *Cache {
- c, err := NewCache(size)
- if err != nil {
- panic(todo("", err))
- }
- return c
- }
- type ConfigOption func(*Config) error
- // Config configures NewPackage
- //
- // Config instances can be shared, they are not mutated once created and
- // configured.
- type Config struct {
- abi *ABI
- buildTagMap map[string]bool
- buildTags []string
- buildTagsKey string // Zero byte separated
- builtin *Package
- cache *Cache
- cmp *Package // Go 1.21
- env map[string]string
- fs fs.FS
- goarch string
- gocompiler string // "gc", "gccgo"
- goos string
- gopath string
- gopathKey string // Zero byte separated
- goroot string
- goversion string
- lookup func(rel, importPath, version string) (fsPath string, err error)
- parallel *parallel
- searchGoPaths []string
- searchGoroot []string
- int Type // Set by NewConfig
- uint Type // Set by NewConfig
- arch32bit bool
- configured bool
- }
- // NewConfig returns a newly created config or an error, if any.
- func NewConfig(opts ...ConfigOption) (r *Config, err error) {
- r = &Config{
- buildTagMap: map[string]bool{},
- env: map[string]string{},
- parallel: newParallel(),
- }
- defer func() {
- if r != nil {
- r.configured = true
- }
- }()
- r.lookup = r.DefaultLookup
- ctx := build.Default
- r.goos = r.getenv("GOOS", ctx.GOOS)
- r.goarch = r.getenv("GOARCH", ctx.GOARCH)
- r.goroot = r.getenv("GOROOT", ctx.GOROOT)
- r.gopath = r.getenv("GOPATH", ctx.GOPATH)
- r.buildTags = append(r.buildTags, r.goos, r.goarch)
- r.gocompiler = runtime.Compiler
- for _, opt := range opts {
- if err := opt(r); err != nil {
- return nil, err
- }
- }
- if r.abi, err = NewABI(r.goos, r.goarch); err != nil {
- return nil, err
- }
- switch r.goarch {
- case "386", "arm":
- r.arch32bit = true
- }
- // During a particular build, the following build tags are satisfied:
- //
- // the target operating system, as spelled by runtime.GOOS, set with the GOOS environment variable.
- // the target architecture, as spelled by runtime.GOARCH, set with the GOARCH environment variable.
- // "unix", if GOOS is a Unix or Unix-like system.
- // the compiler being used, either "gc" or "gccgo"
- // "cgo", if the cgo command is supported (see CGO_ENABLED in 'go help environment').
- // a term for each Go major release, through the current version: "go1.1" from Go version 1.1 onward, "go1.12" from Go 1.12, and so on.
- // any additional tags given by the -tags flag (see 'go help build').
- // There are no separate build tags for beta or minor releases.
- if r.goversion == "" {
- r.goversion = runtime.Version()
- }
- if !strings.HasPrefix(r.goversion, "go") || !strings.Contains(r.goversion, ".") {
- return nil, fmt.Errorf("cannot parse Go version: %s", r.goversion)
- }
- ver := strings.SplitN(r.goversion[len("go"):], ".", 2)
- verMajor, err := strconv.Atoi(ver[0])
- if err != nil {
- return nil, fmt.Errorf("cannot parse Go version %s: %v", r.goversion, err)
- }
- if verMajor != 1 {
- return nil, fmt.Errorf("unsupported Go version: %s", r.goversion)
- }
- switch x, x2 := strings.IndexByte(ver[1], '.'), strings.Index(ver[1], "rc"); {
- case x >= 0:
- ver[1] = ver[1][:x]
- case x2 >= 0:
- ver[1] = ver[1][:x2]
- }
- verMinor, err := strconv.Atoi(ver[1])
- if err != nil {
- return nil, fmt.Errorf("cannot parse Go version %s: %v", r.goversion, err)
- }
- for i := 1; i <= verMinor; i++ {
- r.buildTags = append(r.buildTags, fmt.Sprintf("go%d.%d", verMajor, i))
- }
- r.buildTags = append(r.buildTags, r.gocompiler)
- r.buildTags = append(r.buildTags, extraTags(verMajor, verMinor, r.goos, r.goarch)...)
- if r.getenv("CGO_ENABLED", "1") == "1" {
- r.buildTags = append(r.buildTags, "cgo")
- }
- for i, v := range r.buildTags {
- tag := strings.TrimSpace(v)
- r.buildTags[i] = tag
- r.buildTagMap[tag] = true
- }
- sort.Strings(r.buildTags)
- r.buildTagsKey = strings.Join(r.buildTags, "\x00")
- r.searchGoroot = []string{filepath.Join(r.goroot, "src")}
- r.searchGoPaths = filepath.SplitList(r.gopath)
- r.gopathKey = strings.Join(r.searchGoPaths, "\x00")
- for i, v := range r.searchGoPaths {
- r.searchGoPaths[i] = filepath.Join(v, "src")
- }
- switch r.cmp, err = r.NewPackage("", "cmp", "", nil, false, TypeCheckNone); {
- case err != nil:
- r.cmp = nil
- default:
- //TODO r.cmp.Scope.kind = UniverseScope
- }
- if r.builtin, err = r.NewPackage("", "builtin", "", nil, false, TypeCheckNone); err != nil {
- return nil, err
- }
- r.builtin.Scope.kind = UniverseScope
- if err := r.builtin.check(newCtx(r)); err != nil {
- return nil, err
- }
- return r, nil
- }
- func (c *Config) universe() *Scope {
- if c.builtin != nil {
- return c.builtin.Scope
- }
- return nil
- }
- func (c *Config) stat(name string) (fs.FileInfo, error) {
- if c.fs == nil {
- return os.Stat(name)
- }
- name = filepath.ToSlash(name)
- if x, ok := c.fs.(fs.StatFS); ok {
- return x.Stat(name)
- }
- f, err := c.fs.Open(name)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- return f.Stat()
- }
- func (c *Config) open(name string) (fs.File, error) {
- if c.fs == nil {
- return os.Open(name)
- }
- name = filepath.ToSlash(name)
- return c.fs.Open(name)
- }
- func (c *Config) glob(pattern string) (matches []string, err error) {
- if c.fs == nil {
- return filepath.Glob(pattern)
- }
- pattern = filepath.ToSlash(pattern)
- return fs.Glob(c.fs, pattern)
- }
- func (c *Config) checkConstraints(pos token.Position, sep string) (r bool) {
- if !strings.Contains(sep, "//go:build") && !strings.Contains(sep, "+build") {
- return true
- }
- // defer func() { trc("", r) }()
- lines := strings.Split(sep, "\n")
- var build, plusBuild []string
- for i, line := range lines {
- if constraint.IsGoBuild(line) && i < len(lines)-1 && lines[i+1] == "" {
- build = append(build, line)
- }
- if constraint.IsPlusBuild(line) {
- plusBuild = append(plusBuild, line)
- }
- }
- switch len(build) {
- case 0:
- // ok
- case 1:
- expr, err := constraint.Parse(build[0])
- if err != nil {
- return true
- }
- return expr.Eval(func(tag string) (r bool) {
- // defer func() { trc("%q: %v", tag, r) }()
- switch tag {
- case "unix":
- return unixOS[c.goos]
- default:
- return c.buildTagMap[tag]
- }
- })
- default:
- panic(todo("%v: %q", pos, build))
- }
- for _, line := range plusBuild {
- expr, err := constraint.Parse(line)
- if err != nil {
- return true
- }
- if !expr.Eval(func(tag string) (r bool) {
- // defer func() { trc("%q: %v", tag, r) }()
- switch tag {
- case "unix":
- return unixOS[c.goos]
- default:
- return c.buildTagMap[tag]
- }
- }) {
- return false
- }
- }
- return true
- }
- // Default lookup translates import paths, possibly relative to rel, to file system paths.
- func (c *Config) DefaultLookup(rel, importPath, version string) (fsPath string, err error) {
- if importPath == "" {
- return "", fmt.Errorf("import path cannot be emtpy")
- }
- // Implementation restriction: A compiler may restrict ImportPaths to non-empty
- // strings using only characters belonging to Unicode's L, M, N, P, and S
- // general categories (the Graphic characters without spaces) and may also
- // exclude the characters !"#$%&'()*,:;<=>?[\]^`{|} and the Unicode replacement
- // character U+FFFD.
- if strings.ContainsAny(importPath, "!\"#$%&'()*,:;<=>?[\\]^`{|}\ufffd") {
- return "", fmt.Errorf("invalid import path: %s", importPath)
- }
- for _, r := range importPath {
- if !unicode.Is(unicode.L, r) &&
- !unicode.Is(unicode.M, r) &&
- !unicode.Is(unicode.N, r) &&
- !unicode.Is(unicode.P, r) &&
- !unicode.Is(unicode.S, r) {
- return "", fmt.Errorf("invalid import path: %s", importPath)
- }
- }
- var search []string
- ip0 := importPath
- switch slash := strings.IndexByte(importPath, '/'); {
- case strings.HasPrefix(importPath, "./"):
- if rel != "" {
- panic(todo(""))
- }
- return "", fmt.Errorf("invalid import path: %s", importPath)
- case strings.HasPrefix(importPath, "/"):
- return importPath, nil
- case slash > 0:
- ip0 = importPath[:slash]
- default:
- ip0 = importPath
- }
- if ip0 != "" {
- switch {
- case strings.Contains(ip0, "."):
- search = c.searchGoPaths
- default:
- search = c.searchGoroot
- }
- }
- for _, v := range search {
- fsPath = filepath.Join(v, importPath)
- dir, err := c.open(fsPath)
- if err != nil {
- continue
- }
- fi, err := dir.Stat()
- dir.Close()
- if err != nil {
- continue
- }
- if fi.IsDir() {
- return fsPath, nil
- }
- }
- return "", fmt.Errorf("cannot find package %s, searched %v", importPath, search)
- }
- func (c *Config) getenv(nm, deflt string) (r string) {
- if r = c.env[nm]; r != "" {
- return r
- }
- if r = os.Getenv(nm); r != "" {
- return r
- }
- return deflt
- }
- func DefaultFileFilter(cfg *Config, importPath string, matchedFSPaths []string, withTestFiles bool) (pkgFiles []string, err error) {
- w := 0
- for _, v := range matchedFSPaths {
- base := filepath.Base(v)
- base = base[:len(base)-len(filepath.Ext(base))]
- const testSuffix = "_test"
- if strings.HasSuffix(base, testSuffix) {
- if !withTestFiles {
- continue
- }
- base = base[:len(base)-len(testSuffix)]
- }
- if x := strings.LastIndexByte(base, '_'); x > 0 {
- last := base[x+1:]
- base = base[:x]
- var prevLast string
- if x := strings.LastIndexByte(base, '_'); x > 0 {
- prevLast = base[x+1:]
- }
- if last != "" && prevLast != "" {
- // *_GOOS_GOARCH
- if knownOS[prevLast] && prevLast != cfg.goos {
- continue
- }
- if knownArch[last] && last != cfg.goarch {
- continue
- }
- }
- if last != "" {
- // *_GOOS or *_GOARCH
- if knownOS[last] && last != cfg.goos {
- continue
- }
- if knownArch[last] && last != cfg.goarch {
- continue
- }
- }
- }
- matchedFSPaths[w] = v
- w++
- }
- return matchedFSPaths[:w], nil
- }
- // ConfigBuildTags configures build tags.
- func ConfigBuildTags(tags []string) ConfigOption {
- return func(cfg *Config) error {
- if cfg.configured {
- return fmt.Errorf("ConfigBuildTags: Config instance already configured")
- }
- cfg.buildTags = append(cfg.buildTags, tags...)
- return nil
- }
- }
- // ConfigEnviron configures environment variables.
- func ConfigEnviron(env []string) ConfigOption {
- return func(cfg *Config) error {
- if cfg.configured {
- return fmt.Errorf("ConfigEnviron: Config instance already configured")
- }
- for _, v := range env {
- switch x := strings.IndexByte(v, '='); {
- case x < 0:
- cfg.env[v] = ""
- default:
- cfg.env[v[:x]] = v[x+1:]
- }
- }
- return nil
- }
- }
- // ConfigFS configures a file system used for opening Go source files. If not
- // explicitly configured, a default os.DirFS("/") is used on Unix-like
- // operating systems. On Windows it will be rooted on the volume where
- // runtime.GOROOT() is.
- func ConfigFS(fs fs.FS) ConfigOption {
- return func(cfg *Config) error {
- if cfg.configured {
- return fmt.Errorf("ConfigFS: Config instance already configured")
- }
- cfg.fs = fs
- return nil
- }
- }
- // ConfigLookup configures a lookup function.
- func ConfigLookup(f func(dir, importPath, version string) (fsPath string, err error)) ConfigOption {
- return func(cfg *Config) error {
- if cfg.configured {
- return fmt.Errorf("ConfigLookup: Config instance already configured")
- }
- cfg.lookup = f
- return nil
- }
- }
- // ConfigCache configures a cache.
- func ConfigCache(c *Cache) ConfigOption {
- return func(cfg *Config) error {
- if cfg.configured {
- return fmt.Errorf("ConfigCache: Config instance already configured")
- }
- cfg.cache = c
- return nil
- }
- }
- type importGuard struct {
- m map[string]struct{}
- stack []string
- }
- func newImportGuard() *importGuard { return &importGuard{m: map[string]struct{}{}} }
- // Package represents a Go package. The instance must not be mutated.
- type Package struct {
- AST map[string]*AST // AST maps fsPaths of individual files to their respective ASTs
- FSPath string
- GoFiles []fs.FileInfo
- ImportPath string
- InvalidGoFiles map[string]error // errors for particular files, if any
- Name Token
- Scope *Scope // Package scope.
- Version string
- cfg *Config
- guard *importGuard
- mu sync.Mutex
- typeCheck TypeCheck
- isUnsafe bool // ImportPath == "usnafe"
- // isChecked bool
- }
- // NewPackage returns a Package, possibly cached, for importPath@version or an
- // error, if any. The fileFilter argument can be nil, in such case
- // DefaultFileFilter is used, which ignores Files with suffix _test.go unless
- // withTestFiles is true.
- //
- // NewPackage is safe for concurrent use by multiple goroutines.
- func (c *Config) NewPackage(dir, importPath, version string, fileFilter FileFilter, withTestFiles bool, typeCheck TypeCheck) (pkg *Package, err error) {
- return c.newPackage(dir, importPath, version, fileFilter, withTestFiles, typeCheck, newImportGuard())
- }
- func (c *Config) newPackage(dir, importPath, version string, fileFilter FileFilter, withTestFiles bool, typeCheck TypeCheck, guard *importGuard) (pkg *Package, err error) {
- if _, ok := guard.m[importPath]; ok {
- return nil, fmt.Errorf("import cycle %v", guard.stack)
- }
- guard.stack = append(guard.stack, importPath)
- fsPath, err := c.lookup(dir, importPath, version)
- if err != nil {
- return nil, fmt.Errorf("lookup %s: %v", importPath, err)
- }
- pat := filepath.Join(fsPath, "*.go")
- matches, err := c.glob(pat)
- if err != nil {
- return nil, fmt.Errorf("glob %s: %v", pat, err)
- }
- if len(matches) == 0 {
- return nil, fmt.Errorf("no Go files in %s", fsPath)
- }
- if fileFilter == nil {
- fileFilter = DefaultFileFilter
- }
- if matches, err = fileFilter(c, importPath, matches, withTestFiles); err != nil {
- return nil, fmt.Errorf("matching Go files in %s: %v", fsPath, err)
- }
- var k cacheKey
- if c.cache != nil {
- k = cacheKey{
- buildTagsKey: c.buildTagsKey,
- cfg: c,
- fsPath: fsPath,
- goarch: c.goarch,
- goos: c.goos,
- gopathKey: c.gopathKey,
- goroot: c.goroot,
- importPath: importPath,
- typeCheck: typeCheck,
- withTestFiles: withTestFiles,
- }
- c.cache.Lock() // ---------------------------------------- lock
- item, ok := c.cache.lru.Get(k)
- if ok {
- c.cache.Unlock() // ---------------------------- unlock
- if pkg = item.wait(); pkg != nil && pkg.matches(&k, matches) {
- return pkg, nil
- }
- }
- item = newCacheItem()
- c.cache.lru.Add(k, item)
- c.cache.Unlock() // ------------------------------------ unlock
- defer func() {
- if pkg != nil && err == nil {
- item.set(pkg)
- }
- }()
- }
- r := &Package{
- AST: map[string]*AST{},
- FSPath: fsPath,
- ImportPath: importPath,
- Scope: newScope(c.universe(), PackageScope),
- Version: version,
- cfg: c,
- guard: guard,
- isUnsafe: importPath == "unsafe",
- typeCheck: typeCheck,
- }
- defer func() { r.guard = nil }()
- sort.Strings(matches)
- defer func() {
- sort.Slice(r.GoFiles, func(i, j int) bool { return r.GoFiles[i].Name() < r.GoFiles[j].Name() })
- if err != nil || len(r.InvalidGoFiles) != 0 || typeCheck == TypeCheckNone {
- return
- }
- //TODO err = r.check(newCtx(c))
- }()
- c.parallel.throttle(func() {
- for _, path := range matches {
- if err = c.newPackageFile(r, path); err != nil {
- return
- }
- }
- })
- return r, err
- }
- func (c *Config) newPackageFile(pkg *Package, path string) (err error) {
- f, err := c.open(path)
- if err != nil {
- return fmt.Errorf("opening file %q: %v", path, err)
- }
- defer func() {
- f.Close()
- if err != nil {
- if pkg.InvalidGoFiles == nil {
- pkg.InvalidGoFiles = map[string]error{}
- }
- pkg.InvalidGoFiles[path] = err
- }
- }()
- var fi fs.FileInfo
- if fi, err = f.Stat(); err != nil {
- return fmt.Errorf("stat %s: %v", path, err)
- }
- if !fi.Mode().IsRegular() {
- return nil
- }
- var b []byte
- if b, err = io.ReadAll(f); err != nil {
- return fmt.Errorf("reading %s: %v", path, err)
- }
- p := newParser(pkg.Scope, path, b, false)
- if p.peek(0) == PACKAGE {
- tok := Token{p.s.source, p.s.toks[p.ix].ch, int32(p.ix)}
- if !c.checkConstraints(tok.Position(), tok.Sep()) {
- return nil
- }
- }
- pkg.GoFiles = append(pkg.GoFiles, fi)
- var ast *AST
- if ast, err = p.parse(); err != nil {
- return nil
- }
- pkg.AST[path] = ast
- return nil
- }
- func (p *Package) matches(k *cacheKey, matches []string) bool {
- matched := map[string]struct{}{}
- for _, match := range matches {
- matched[match] = struct{}{}
- }
- for _, cachedInfo := range p.GoFiles {
- name := cachedInfo.Name()
- path := filepath.Join(p.FSPath, name)
- if _, ok := matched[path]; !ok {
- return false
- }
- info, err := k.cfg.stat(path)
- if err != nil {
- return false
- }
- if info.IsDir() ||
- info.Size() != cachedInfo.Size() ||
- info.ModTime().After(cachedInfo.ModTime()) ||
- info.Mode() != cachedInfo.Mode() {
- return false
- }
- }
- return true
- }
- // ParseFile parses 'b', assuming it comes from 'path' and returns an AST or error, if any.
- func ParseFile(path string, b []byte) (*AST, error) {
- return newParser(newScope(nil, PackageScope), path, b, false).parse()
- }
|