blob: 9f6e9ad002a5949ee9bdd88cfdc31fd747cbb8cb [file] [log] [blame]
// Copyright 2018 The CUE Authors
//
// 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 load
import (
"bytes"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
"cuelang.org/go/internal/filetypes"
)
// An importMode controls the behavior of the Import method.
type importMode uint
const (
// If findOnly is set, Import stops after locating the directory
// that should contain the sources for a package. It does not
// read any files in the directory.
findOnly importMode = 1 << iota
// If importComment is set, parse import comments on package statements.
// Import returns an error if it finds a comment it cannot understand
// or finds conflicting comments in multiple source files.
// See golang.org/s/go14customimport for more information.
importComment
allowAnonymous
)
// importPkg returns details about the CUE package named by the import path,
// interpreting local import paths relative to the srcDir directory.
// If the path is a local import path naming a package that can be imported
// using a standard import path, the returned package will set p.ImportPath
// to that path.
//
// In the directory and ancestor directories up to including one with a
// cue.mod file, all .cue files are considered part of the package except for:
//
// - files starting with _ or . (likely editor temporary files)
// - files with build constraints not satisfied by the context
//
// If an error occurs, importPkg sets the error in the returned instance,
// which then may contain partial information.
//
// pkgName indicates which packages to load. It supports the following
// values:
// "" the default package for the directory, if only one
// is present.
// _ anonymous files (which may be marked with _)
// * all packages
//
func (l *loader) importPkg(pos token.Pos, p *build.Instance) []*build.Instance {
l.stk.Push(p.ImportPath)
defer l.stk.Pop()
cfg := l.cfg
ctxt := &cfg.fileSystem
if p.Err != nil {
return []*build.Instance{p}
}
retErr := func(errs errors.Error) []*build.Instance {
// XXX: move this loop to ReportError
for _, err := range errors.Errors(errs) {
p.ReportError(err)
}
return []*build.Instance{p}
}
if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) {
err := errors.Newf(token.NoPos, "module root not defined", p.DisplayPath)
return retErr(err)
}
fp := newFileProcessor(cfg, p)
if p.PkgName == "" {
if l.cfg.Package == "*" {
fp.ignoreOther = true
fp.allPackages = true
p.PkgName = "_"
} else {
p.PkgName = l.cfg.Package
}
}
if p.PkgName != "" {
// If we have an explicit package name, we can ignore other packages.
fp.ignoreOther = true
}
if !strings.HasPrefix(p.Dir, cfg.ModuleRoot) {
panic("")
}
var dirs [][2]string
genDir := GenPath(cfg.ModuleRoot)
if strings.HasPrefix(p.Dir, genDir) {
dirs = append(dirs, [2]string{genDir, p.Dir})
// TODO(legacy): don't support "pkg"
// && p.PkgName != "_"
if filepath.Base(genDir) != "pkg" {
for _, sub := range []string{"pkg", "usr"} {
rel, err := filepath.Rel(genDir, p.Dir)
if err != nil {
// should not happen
return retErr(
errors.Wrapf(err, token.NoPos, "invalid path"))
}
base := filepath.Join(cfg.ModuleRoot, modDir, sub)
dir := filepath.Join(base, rel)
dirs = append(dirs, [2]string{base, dir})
}
}
} else {
dirs = append(dirs, [2]string{cfg.ModuleRoot, p.Dir})
}
found := false
for _, d := range dirs {
info, err := ctxt.stat(d[1])
if err == nil && info.IsDir() {
found = true
break
}
}
if !found {
return retErr(
&PackageError{
Message: errors.NewMessage("cannot find package %q",
[]interface{}{p.DisplayPath}),
})
}
// This algorithm assumes that multiple directories within cue.mod/*/
// have the same module scope and that there are no invalid modules.
inModule := false // if pkg == "_"
for _, d := range dirs {
if l.cfg.findRoot(d[1]) != "" {
inModule = true
break
}
}
for _, d := range dirs {
for dir := filepath.Clean(d[1]); ctxt.isDir(dir); {
files, err := ctxt.readDir(dir)
if err != nil && !os.IsNotExist(err) {
return retErr(errors.Wrapf(err, pos, "import failed reading dir %v", dirs[0][1]))
}
for _, f := range files {
if f.IsDir() {
continue
}
if f.Name() == "-" {
if _, err := cfg.fileSystem.stat("-"); !os.IsNotExist(err) {
continue
}
}
file, err := filetypes.ParseFile(f.Name(), filetypes.Input)
if err != nil {
p.UnknownFiles = append(p.UnknownFiles, &build.File{
Filename: f.Name(),
})
continue // skip unrecognized file types
}
fp.add(pos, dir, file, importComment)
}
if p.PkgName == "" || !inModule || l.cfg.isRoot(dir) || dir == d[0] {
break
}
// From now on we just ignore files that do not belong to the same
// package.
fp.ignoreOther = true
parent, _ := filepath.Split(dir)
parent = filepath.Clean(parent)
if parent == dir || len(parent) < len(d[0]) {
break
}
dir = parent
}
}
all := []*build.Instance{}
for _, p := range fp.pkgs {
impPath, err := addImportQualifier(importPath(p.ImportPath), p.PkgName)
p.ImportPath = string(impPath)
if err != nil {
p.ReportError(err)
}
all = append(all, p)
rewriteFiles(p, cfg.ModuleRoot, false)
if errs := fp.finalize(p); errs != nil {
p.ReportError(errs)
return all
}
l.addFiles(cfg.ModuleRoot, p)
_ = p.Complete()
}
sort.Slice(all, func(i, j int) bool {
return all[i].Dir < all[j].Dir
})
return all
}
// loadFunc creates a LoadFunc that can be used to create new build.Instances.
func (l *loader) loadFunc() build.LoadFunc {
return func(pos token.Pos, path string) *build.Instance {
cfg := l.cfg
impPath := importPath(path)
if isLocalImport(path) {
return cfg.newErrInstance(pos, impPath,
errors.Newf(pos, "relative import paths not allowed (%q)", path))
}
// is it a builtin?
if strings.IndexByte(strings.Split(path, "/")[0], '.') == -1 {
if l.cfg.StdRoot != "" {
p := cfg.newInstance(pos, impPath)
_ = l.importPkg(pos, p)
return p
}
return nil
}
p := cfg.newInstance(pos, impPath)
_ = l.importPkg(pos, p)
return p
}
}
func normPrefix(root, path string, isLocal bool) string {
root = filepath.Clean(root)
prefix := ""
if isLocal {
prefix = "." + string(filepath.Separator)
}
if !strings.HasSuffix(root, string(filepath.Separator)) &&
strings.HasPrefix(path, root) {
path = prefix + path[len(root)+1:]
}
return path
}
func rewriteFiles(p *build.Instance, root string, isLocal bool) {
p.Root = root
normalizeFilenames(root, p.CUEFiles, isLocal)
normalizeFilenames(root, p.TestCUEFiles, isLocal)
normalizeFilenames(root, p.ToolCUEFiles, isLocal)
normalizeFilenames(root, p.IgnoredCUEFiles, isLocal)
normalizeFilenames(root, p.InvalidCUEFiles, isLocal)
normalizeFiles(p.BuildFiles)
normalizeFiles(p.IgnoredFiles)
normalizeFiles(p.OrphanedFiles)
normalizeFiles(p.InvalidFiles)
normalizeFiles(p.UnknownFiles)
}
func normalizeFilenames(root string, a []string, isLocal bool) {
for i, path := range a {
if strings.HasPrefix(path, root) {
a[i] = normPrefix(root, path, isLocal)
}
}
sortParentsFirst(a)
}
func sortParentsFirst(s []string) {
sort.Slice(s, func(i, j int) bool {
return len(filepath.Dir(s[i])) < len(filepath.Dir(s[j]))
})
}
func normalizeFiles(a []*build.File) {
sort.Slice(a, func(i, j int) bool {
return len(filepath.Dir(a[i].Filename)) < len(filepath.Dir(a[j].Filename))
})
}
type fileProcessor struct {
firstFile string
firstCommentFile string
imported map[string][]token.Pos
allTags map[string]bool
allFiles bool
ignoreOther bool // ignore files from other packages
allPackages bool
c *Config
pkgs map[string]*build.Instance
pkg *build.Instance
err errors.Error
}
func newFileProcessor(c *Config, p *build.Instance) *fileProcessor {
return &fileProcessor{
imported: make(map[string][]token.Pos),
allTags: make(map[string]bool),
c: c,
pkgs: map[string]*build.Instance{"_": p},
pkg: p,
}
}
func countCUEFiles(c *Config, p *build.Instance) int {
count := len(p.CUEFiles)
if c.Tools {
count += len(p.ToolCUEFiles)
}
if c.Tests {
count += len(p.TestCUEFiles)
}
return count
}
func (fp *fileProcessor) finalize(p *build.Instance) errors.Error {
if fp.err != nil {
return fp.err
}
if countCUEFiles(fp.c, p) == 0 &&
!fp.c.DataFiles &&
(p.PkgName != "_" || !fp.allPackages) {
fp.err = errors.Append(fp.err, &NoFilesError{Package: p, ignored: len(p.IgnoredCUEFiles) > 0})
return fp.err
}
for tag := range fp.allTags {
p.AllTags = append(p.AllTags, tag)
}
sort.Strings(p.AllTags)
p.ImportPaths, _ = cleanImports(fp.imported)
return nil
}
func (fp *fileProcessor) add(pos token.Pos, root string, file *build.File, mode importMode) (added bool) {
fullPath := file.Filename
if fullPath != "-" {
if !filepath.IsAbs(fullPath) {
fullPath = filepath.Join(root, fullPath)
}
}
file.Filename = fullPath
base := filepath.Base(fullPath)
// special * and _
p := fp.pkg // default package
// badFile := func(p *build.Instance, err errors.Error) bool {
badFile := func(err errors.Error) bool {
fp.err = errors.Append(fp.err, err)
p.InvalidCUEFiles = append(p.InvalidCUEFiles, fullPath)
p.InvalidFiles = append(p.InvalidFiles, file)
return true
}
match, data, err := matchFile(fp.c, file, true, fp.allFiles, fp.allTags)
if err != nil {
return badFile(err)
}
if !match {
if file.Encoding == build.CUE && file.Interpretation == "" {
p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
p.IgnoredFiles = append(p.IgnoredFiles, file)
} else {
p.OrphanedFiles = append(p.OrphanedFiles, file)
p.DataFiles = append(p.DataFiles, fullPath)
}
return false // don't mark as added
}
pf, perr := parser.ParseFile(fullPath, data, parser.ImportsOnly, parser.ParseComments)
if perr != nil {
badFile(errors.Promote(perr, "add failed"))
return true
}
_, pkg, _ := internal.PackageInfo(pf)
if pkg == "" {
pkg = "_"
}
switch {
case pkg == p.PkgName, mode&allowAnonymous != 0:
case fp.allPackages && pkg != "_":
q := fp.pkgs[pkg]
if q == nil {
q = &build.Instance{
PkgName: pkg,
Dir: p.Dir,
DisplayPath: p.DisplayPath,
ImportPath: p.ImportPath + ":" + pkg,
Root: p.Root,
Module: p.Module,
}
fp.pkgs[pkg] = q
}
p = q
case pkg != "_":
default:
p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
p.IgnoredFiles = append(p.IgnoredFiles, file)
return false // don't mark as added
}
if !fp.c.AllCUEFiles {
if include, err := shouldBuildFile(pf, fp); !include {
if err != nil {
fp.err = errors.Append(fp.err, err)
}
p.IgnoredCUEFiles = append(p.InvalidCUEFiles, fullPath)
p.IgnoredFiles = append(p.InvalidFiles, file)
return false
}
}
if pkg != "" && pkg != "_" {
if p.PkgName == "" {
p.PkgName = pkg
fp.firstFile = base
} else if pkg != p.PkgName {
if fp.ignoreOther {
p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
p.IgnoredFiles = append(p.IgnoredFiles, file)
return false
}
return badFile(&MultiplePackageError{
Dir: p.Dir,
Packages: []string{p.PkgName, pkg},
Files: []string{fp.firstFile, base},
})
}
}
isTest := strings.HasSuffix(base, "_test"+cueSuffix)
isTool := strings.HasSuffix(base, "_tool"+cueSuffix)
if mode&importComment != 0 {
qcom, line := findimportComment(data)
if line != 0 {
com, err := strconv.Unquote(qcom)
if err != nil {
badFile(errors.Newf(pos, "%s:%d: cannot parse import comment", fullPath, line))
} else if p.ImportComment == "" {
p.ImportComment = com
fp.firstCommentFile = base
} else if p.ImportComment != com {
badFile(errors.Newf(pos, "found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, base, p.Dir))
}
}
}
for _, decl := range pf.Decls {
d, ok := decl.(*ast.ImportDecl)
if !ok {
continue
}
for _, spec := range d.Specs {
quoted := spec.Path.Value
path, err := strconv.Unquote(quoted)
if err != nil {
badFile(errors.Newf(
spec.Path.Pos(),
"%s: parser returned invalid quoted string: <%s>", fullPath, quoted,
))
}
if !isTest || fp.c.Tests {
fp.imported[path] = append(fp.imported[path], spec.Pos())
}
}
}
switch {
case isTest:
p.TestCUEFiles = append(p.TestCUEFiles, fullPath)
if fp.c.loader.cfg.Tests {
p.BuildFiles = append(p.BuildFiles, file)
} else {
p.IgnoredFiles = append(p.IgnoredFiles, file)
}
case isTool:
p.ToolCUEFiles = append(p.ToolCUEFiles, fullPath)
if fp.c.loader.cfg.Tools {
p.BuildFiles = append(p.BuildFiles, file)
} else {
p.IgnoredFiles = append(p.IgnoredFiles, file)
}
default:
p.CUEFiles = append(p.CUEFiles, fullPath)
p.BuildFiles = append(p.BuildFiles, file)
}
return true
}
func nameExt(name string) string {
i := strings.LastIndex(name, ".")
if i < 0 {
return ""
}
return name[i:]
}
// hasCUEFiles reports whether dir contains any files with names ending in .go.
// For a vendor check we must exclude directories that contain no .go files.
// Otherwise it is not possible to vendor just a/b/c and still import the
// non-vendored a/b. See golang.org/issue/13832.
func hasCUEFiles(ctxt *fileSystem, dir string) bool {
ents, _ := ctxt.readDir(dir)
for _, ent := range ents {
if !ent.IsDir() && strings.HasSuffix(ent.Name(), cueSuffix) {
return true
}
}
return false
}
func findimportComment(data []byte) (s string, line int) {
// expect keyword package
word, data := parseWord(data)
if string(word) != "package" {
return "", 0
}
// expect package name
_, data = parseWord(data)
// now ready for import comment, a // comment
// beginning and ending on the current line.
for len(data) > 0 && (data[0] == ' ' || data[0] == '\t' || data[0] == '\r') {
data = data[1:]
}
var comment []byte
switch {
case bytes.HasPrefix(data, slashSlash):
i := bytes.Index(data, newline)
if i < 0 {
i = len(data)
}
comment = data[2:i]
}
comment = bytes.TrimSpace(comment)
// split comment into `import`, `"pkg"`
word, arg := parseWord(comment)
if string(word) != "import" {
return "", 0
}
line = 1 + bytes.Count(data[:cap(data)-cap(arg)], newline)
return strings.TrimSpace(string(arg)), line
}
var (
slashSlash = []byte("//")
newline = []byte("\n")
)
// skipSpaceOrComment returns data with any leading spaces or comments removed.
func skipSpaceOrComment(data []byte) []byte {
for len(data) > 0 {
switch data[0] {
case ' ', '\t', '\r', '\n':
data = data[1:]
continue
case '/':
if bytes.HasPrefix(data, slashSlash) {
i := bytes.Index(data, newline)
if i < 0 {
return nil
}
data = data[i+1:]
continue
}
}
break
}
return data
}
// parseWord skips any leading spaces or comments in data
// and then parses the beginning of data as an identifier or keyword,
// returning that word and what remains after the word.
func parseWord(data []byte) (word, rest []byte) {
data = skipSpaceOrComment(data)
// Parse past leading word characters.
rest = data
for {
r, size := utf8.DecodeRune(rest)
if unicode.IsLetter(r) || '0' <= r && r <= '9' || r == '_' {
rest = rest[size:]
continue
}
break
}
word = data[:len(data)-len(rest)]
if len(word) == 0 {
return nil, nil
}
return word, rest
}
func cleanImports(m map[string][]token.Pos) ([]string, map[string][]token.Pos) {
all := make([]string, 0, len(m))
for path := range m {
all = append(all, path)
}
sort.Strings(all)
return all, m
}
// // Import is shorthand for Default.Import.
// func Import(path, srcDir string, mode ImportMode) (*Package, error) {
// return Default.Import(path, srcDir, mode)
// }
// // ImportDir is shorthand for Default.ImportDir.
// func ImportDir(dir string, mode ImportMode) (*Package, error) {
// return Default.ImportDir(dir, mode)
// }
var slashslash = []byte("//")
// isLocalImport reports whether the import path is
// a local import path, like ".", "..", "./foo", or "../foo".
func isLocalImport(path string) bool {
return path == "." || path == ".." ||
strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
}