blob: adc0bae6607e51051d121ec36bffedd739a9275a [file] [log] [blame]
// Copyright 2021 Google LLC
//
// 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 pkgload
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"sort"
"strconv"
"strings"
"cloud.google.com/go/third_party/go/doc"
"golang.org/x/tools/go/packages"
)
// Info holds info about a package.
type Info struct {
Pkg *packages.Package
Doc *doc.Package
Fset *token.FileSet
// ImportRenames is a map from package path to local name or "".
ImportRenames map[string]string
Status string
}
// Load parses the given glob and returns info for the matching packages.
// The workingDir is used for module detection.
// Packages that match the filter are ignored.
func Load(glob, workingDir string, filter []string) ([]Info, error) {
config := &packages.Config{
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule | packages.NeedImports | packages.NeedDeps,
Tests: true,
Dir: workingDir,
}
allPkgs, err := packages.Load(config, glob)
if err != nil {
return nil, fmt.Errorf("packages.Load: %v", err)
}
packages.PrintErrors(allPkgs) // Don't fail everything because of one package.
if len(allPkgs) == 0 {
return nil, fmt.Errorf("pattern %q matched 0 packages", glob)
}
module := allPkgs[0].Module
skippedModules := map[string]struct{}{}
// First, collect all of the files grouped by package, including test
// packages.
pkgFiles := map[string][]string{}
idToPkg := map[string]*packages.Package{}
pkgNames := []string{}
for _, pkg := range allPkgs {
// Ignore filtered packages.
if hasPrefix(pkg.PkgPath, filter) {
continue
}
id := pkg.ID
// See https://pkg.go.dev/golang.org/x/tools/go/packages#Config.
// The uncompiled test package shows up as "foo_test [foo.test]".
if strings.HasSuffix(id, ".test") ||
strings.Contains(id, "internal") ||
strings.Contains(id, "third_party") ||
(strings.Contains(id, " [") && !strings.Contains(id, "_test [")) {
continue
}
if strings.Contains(id, "_test") {
id = id[0:strings.Index(id, "_test [")]
} else if pkg.Module != nil {
idToPkg[pkg.PkgPath] = pkg
pkgNames = append(pkgNames, pkg.PkgPath)
// The test package doesn't have Module set.
if pkg.Module.Path != module.Path {
skippedModules[pkg.Module.Path] = struct{}{}
continue
}
}
for _, f := range pkg.Syntax {
name := pkg.Fset.File(f.Pos()).Name()
if strings.HasSuffix(name, ".go") {
pkgFiles[id] = append(pkgFiles[id], name)
}
}
}
sort.Strings(pkgNames)
result := []Info{}
for _, pkgPath := range pkgNames {
// Check if pkgPath has prefix of skipped module.
skip := false
for skipModule := range skippedModules {
if strings.HasPrefix(pkgPath, skipModule) {
skip = true
break
}
}
if skip {
continue
}
parsedFiles := []*ast.File{}
fset := token.NewFileSet()
for _, f := range pkgFiles[pkgPath] {
pf, err := parser.ParseFile(fset, f, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("ParseFile: %v", err)
}
parsedFiles = append(parsedFiles, pf)
}
// Parse out GoDoc.
docPkg, err := doc.NewFromFiles(fset, parsedFiles, pkgPath)
if err != nil {
return nil, fmt.Errorf("doc.NewFromFiles: %v", err)
}
// Extra filter in case the file filtering didn't catch everything.
if !strings.HasPrefix(docPkg.ImportPath, module.Path) {
continue
}
imports := map[string]string{}
for _, f := range parsedFiles {
for _, i := range f.Imports {
name := ""
// i.Name is nil for imports that aren't renamed.
if i.Name != nil {
name = i.Name.Name
}
iPath, err := strconv.Unquote(i.Path.Value)
if err != nil {
return nil, fmt.Errorf("strconv.Unquote: %v", err)
}
imports[iPath] = name
}
}
result = append(result, Info{
Pkg: idToPkg[pkgPath],
Doc: docPkg,
Fset: fset,
ImportRenames: imports,
Status: pkgStatus(pkgPath, docPkg.Doc, idToPkg[pkgPath].Module.Version),
})
}
return result, nil
}
// pkgStatus returns the status of the given package with the
// given GoDoc.
//
// pkgStatus does not use repo-metadata-full.json because it's
// not available for all modules nor all versions.
func pkgStatus(importPath, doc, version string) string {
switch {
case strings.Contains(version, "-"):
return "preview"
case strings.Contains(doc, "\nDeprecated:"):
return "deprecated"
case strings.Contains(doc, "This package is in alpha"):
return "alpha"
case strings.Contains(doc, "This package is in beta"):
return "beta"
case strings.Contains(importPath, "alpha"):
return "alpha"
case strings.Contains(importPath, "beta"):
return "beta"
}
return ""
}
func hasPrefix(s string, prefixes []string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(s, prefix) {
return true
}
}
return false
}