blob: 5b31e0da64993918c37db85c88cb3bb756cd61e1 [file]
// Copyright 2021 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"encoding/json"
"fmt"
"go/parser"
"go/token"
"io"
"os"
"strconv"
"strings"
"golang.org/x/tools/go/packages"
)
type ResolvePkgFunc func(importPath string) *packages.Package
// Copy and pasted from golang.org/x/tools/go/packages
type FlatPackagesError struct {
Pos string // "file:line:col" or "file:line" or "" or "-"
Msg string
Kind FlatPackagesErrorKind
}
type FlatPackagesErrorKind int
const (
UnknownError FlatPackagesErrorKind = iota
ListError
ParseError
TypeError
)
func (err FlatPackagesError) Error() string {
pos := err.Pos
if pos == "" {
pos = "-" // like token.Position{}.String()
}
return pos + ": " + err.Msg
}
// FlatPackage is the JSON form of Package
// It drops all the type and syntax fields, and transforms the Imports
type FlatPackage struct {
ID string
Name string `json:",omitempty"`
PkgPath string `json:",omitempty"`
Errors []FlatPackagesError `json:",omitempty"`
GoFiles []string `json:",omitempty"`
CompiledGoFiles []string `json:",omitempty"`
OtherFiles []string `json:",omitempty"`
ExportFile string `json:",omitempty"`
Imports map[string]string `json:",omitempty"`
Standard bool `json:",omitempty"`
}
type (
PackageFunc func(pkg *FlatPackage)
PathResolverFunc func(path string) string
)
func resolvePathsInPlace(prf PathResolverFunc, paths []string) {
for i, path := range paths {
paths[i] = prf(path)
}
}
func WalkFlatPackagesFromJSON(jsonFile string, onPkg PackageFunc) error {
f, err := os.Open(jsonFile)
if err != nil {
return fmt.Errorf("unable to open package JSON file: %w", err)
}
defer f.Close()
decoder := json.NewDecoder(f)
for decoder.More() {
pkg := &FlatPackage{}
if err := decoder.Decode(&pkg); err != nil {
return fmt.Errorf("unable to decode package in %s: %w", f.Name(), err)
}
onPkg(pkg)
}
return nil
}
func ResolvePaths(pak *packages.Package, prf PathResolverFunc) error {
resolvePathsInPlace(prf, pak.CompiledGoFiles)
resolvePathsInPlace(prf, pak.GoFiles)
resolvePathsInPlace(prf, pak.OtherFiles)
pak.ExportFile = prf(pak.ExportFile)
return nil
}
// FilterFilesForBuildTags filters the source files given the current build
// tags.
func FilterFilesForBuildTags(pak *packages.Package) {
pak.CompiledGoFiles = filterSourceFilesForTags(pak.CompiledGoFiles)
}
func filterTestSuffix(pkg *packages.Package, files []string) (err error, testFiles []string, xTestFiles, nonTestFiles []string) {
for _, filename := range files {
if strings.HasSuffix(filename, "_test.go") {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, parser.PackageClauseOnly)
if err != nil {
return err, nil, nil, nil
}
if f.Name.Name == pkg.Name {
testFiles = append(testFiles, filename)
} else {
xTestFiles = append(xTestFiles, filename)
}
} else {
nonTestFiles = append(nonTestFiles, filename)
}
}
return
}
func MoveTestFiles(pkg *packages.Package) *packages.Package {
err, tgf, xtgf, gf := filterTestSuffix(pkg, pkg.GoFiles)
if err != nil {
return nil
}
pkg.GoFiles = append(gf, tgf...)
err, ctgf, cxtgf, cgf := filterTestSuffix(pkg, pkg.CompiledGoFiles)
if err != nil {
return nil
}
pkg.CompiledGoFiles = append(cgf, ctgf...)
if len(xtgf) == 0 && len(cxtgf) == 0 {
return nil
}
newImports := make(map[string]*packages.Package, len(pkg.Imports))
for k, v := range pkg.Imports {
newImports[k] = v
}
newImports[pkg.PkgPath] = &packages.Package{
ID: pkg.ID,
}
// Clone package, only xtgf files
return &packages.Package{
ID: pkg.ID + "_xtest",
Name: pkg.Name + "_test",
PkgPath: pkg.PkgPath + "_test",
Imports: newImports,
Errors: pkg.Errors,
GoFiles: append([]string{}, xtgf...),
CompiledGoFiles: append([]string{}, cxtgf...),
OtherFiles: pkg.OtherFiles,
ExportFile: pkg.ExportFile,
}
}
func (fp *FlatPackage) IsStdlib() bool {
return fp.Standard
}
// ResolveImports resolves imports for non-stdlib packages and integrates file overlays
// to allow modification of package imports without modifying disk files.
func ResolveImports(pkg *packages.Package, resolve ResolvePkgFunc, overlays map[string][]byte) error {
fset := token.NewFileSet()
for _, file := range pkg.CompiledGoFiles {
// Only assign overlayContent when an overlay for the file exists, since ParseFile checks by type.
// If overlay is assigned directly from the map, it will have []byte as type
// Empty []byte types are parsed into io.EOF
var overlayReader io.Reader
if content, ok := overlays[file]; ok {
overlayReader = bytes.NewReader(content)
}
f, err := parser.ParseFile(fset, file, overlayReader, parser.ImportsOnly)
if err != nil {
return err
}
// If the name is not provided, fetch it from the sources
if pkg.Name == "" {
pkg.Name = f.Name.Name
}
for _, rawImport := range f.Imports {
imp, err := strconv.Unquote(rawImport.Path.Value)
if err != nil {
continue
}
// We don't handle CGo for now
if imp == "C" {
continue
}
if _, ok := pkg.Imports[imp]; ok {
continue
}
if impPkg := resolve(imp); impPkg != nil {
pkg.Imports[imp] = impPkg
}
}
}
return nil
}
func (fp *FlatPackage) IsRoot() bool {
return strings.HasPrefix(fp.ID, "//")
}