blob: e62ebe29903b6e89b7a61e22f1e6b45d2a3d995b [file] [log] [blame]
// Copyright 2023 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 cache
import (
"bytes"
"go/build"
"go/parser"
"go/token"
"io"
"path/filepath"
"strings"
"golang.org/x/tools/gopls/internal/util/bug"
)
type port struct{ GOOS, GOARCH string }
var (
// preferredPorts holds GOOS/GOARCH combinations for which we dynamically
// create new Views, by setting GOOS=... and GOARCH=... on top of
// user-provided configuration when we detect that the default build
// configuration does not match an open file. Ports are matched in the order
// defined below, so that when multiple ports match a file we use the port
// occurring at a lower index in the slice. For that reason, we sort first
// class ports ahead of secondary ports, and (among first class ports) 64-bit
// ports ahead of the less common 32-bit ports.
preferredPorts = []port{
// First class ports, from https://go.dev/wiki/PortingPolicy.
{"darwin", "amd64"},
{"darwin", "arm64"},
{"linux", "amd64"},
{"linux", "arm64"},
{"windows", "amd64"},
{"linux", "arm"},
{"linux", "386"},
{"windows", "386"},
// Secondary ports, from GOROOT/src/internal/platform/zosarch.go.
// (First class ports are commented out.)
{"aix", "ppc64"},
{"dragonfly", "amd64"},
{"freebsd", "386"},
{"freebsd", "amd64"},
{"freebsd", "arm"},
{"freebsd", "arm64"},
{"illumos", "amd64"},
{"linux", "ppc64"},
{"linux", "ppc64le"},
{"linux", "mips"},
{"linux", "mipsle"},
{"linux", "mips64"},
{"linux", "mips64le"},
{"linux", "riscv64"},
{"linux", "s390x"},
{"android", "386"},
{"android", "amd64"},
{"android", "arm"},
{"android", "arm64"},
{"ios", "arm64"},
{"ios", "amd64"},
{"js", "wasm"},
{"netbsd", "386"},
{"netbsd", "amd64"},
{"netbsd", "arm"},
{"netbsd", "arm64"},
{"openbsd", "386"},
{"openbsd", "amd64"},
{"openbsd", "arm"},
{"openbsd", "arm64"},
{"openbsd", "mips64"},
{"plan9", "386"},
{"plan9", "amd64"},
{"plan9", "arm"},
{"solaris", "amd64"},
{"windows", "arm"},
{"windows", "arm64"},
{"aix", "ppc64"},
{"android", "386"},
{"android", "amd64"},
{"android", "arm"},
{"android", "arm64"},
// {"darwin", "amd64"},
// {"darwin", "arm64"},
{"dragonfly", "amd64"},
{"freebsd", "386"},
{"freebsd", "amd64"},
{"freebsd", "arm"},
{"freebsd", "arm64"},
{"freebsd", "riscv64"},
{"illumos", "amd64"},
{"ios", "amd64"},
{"ios", "arm64"},
{"js", "wasm"},
// {"linux", "386"},
// {"linux", "amd64"},
// {"linux", "arm"},
// {"linux", "arm64"},
{"linux", "loong64"},
{"linux", "mips"},
{"linux", "mips64"},
{"linux", "mips64le"},
{"linux", "mipsle"},
{"linux", "ppc64"},
{"linux", "ppc64le"},
{"linux", "riscv64"},
{"linux", "s390x"},
{"linux", "sparc64"},
{"netbsd", "386"},
{"netbsd", "amd64"},
{"netbsd", "arm"},
{"netbsd", "arm64"},
{"openbsd", "386"},
{"openbsd", "amd64"},
{"openbsd", "arm"},
{"openbsd", "arm64"},
{"openbsd", "mips64"},
{"openbsd", "ppc64"},
{"openbsd", "riscv64"},
{"plan9", "386"},
{"plan9", "amd64"},
{"plan9", "arm"},
{"solaris", "amd64"},
{"wasip1", "wasm"},
// {"windows", "386"},
// {"windows", "amd64"},
{"windows", "arm"},
{"windows", "arm64"},
}
)
// matches reports whether the port matches a file with the given absolute path
// and content.
//
// Note that this function accepts content rather than e.g. a file.Handle,
// because we trim content before matching for performance reasons, and
// therefore need to do this outside of matches when considering multiple ports.
func (p port) matches(path string, content []byte) bool {
ctxt := build.Default // make a copy
ctxt.UseAllFiles = false
dir, name := filepath.Split(path)
// The only virtualized operation called by MatchFile is OpenFile.
ctxt.OpenFile = func(p string) (io.ReadCloser, error) {
if p != path {
return nil, bug.Errorf("unexpected file %q", p)
}
return io.NopCloser(bytes.NewReader(content)), nil
}
ctxt.GOOS = p.GOOS
ctxt.GOARCH = p.GOARCH
ok, err := ctxt.MatchFile(dir, name)
return err == nil && ok
}
// trimContentForPortMatch trims the given Go file content to a minimal file
// containing the same build constraints, if any.
//
// This is an unfortunate but necessary optimization, as matching build
// constraints using go/build has significant overhead, and involves parsing
// more than just the build constraint.
//
// TestMatchingPortsConsistency enforces consistency by comparing results
// without trimming content.
func trimContentForPortMatch(content []byte) []byte {
buildComment := buildComment(content)
return []byte(buildComment + "\npackage p") // package name does not matter
}
// buildComment returns the first matching //go:build comment in the given
// content, or "" if none exists.
func buildComment(content []byte) string {
f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
return ""
}
for _, cg := range f.Comments {
for _, c := range cg.List {
if isGoBuildComment(c.Text) {
return c.Text
}
}
}
return ""
}
// Adapted from go/build/build.go.
//
// TODO(rfindley): use constraint.IsGoBuild once we are on 1.19+.
func isGoBuildComment(line string) bool {
const goBuildComment = "//go:build"
if !strings.HasPrefix(line, goBuildComment) {
return false
}
// Report whether //go:build is followed by a word boundary.
line = strings.TrimSpace(line)
rest := line[len(goBuildComment):]
return len(rest) == 0 || len(strings.TrimSpace(rest)) < len(rest)
}