| // 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 |
| |
| // Files in package are to a large extent based on Go files from the following |
| // Go packages: |
| // - cmd/go/internal/load |
| // - go/build |
| |
| import ( |
| pathpkg "path" |
| "path/filepath" |
| "strings" |
| "unicode" |
| |
| build "cuelang.org/go/cue/build" |
| "cuelang.org/go/cue/encoding" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/token" |
| ) |
| |
| // Instances returns the instances named by the command line arguments 'args'. |
| // If errors occur trying to load an instance it is returned with Incomplete |
| // set. Errors directly related to loading the instance are recorded in this |
| // instance, but errors that occur loading dependencies are recorded in these |
| // dependencies. |
| func Instances(args []string, c *Config) []*build.Instance { |
| if c == nil { |
| c = &Config{} |
| } |
| newC, err := c.complete() |
| if err != nil { |
| return []*build.Instance{c.newErrInstance(token.NoPos, "", err)} |
| } |
| c = newC |
| |
| l := c.loader |
| |
| // TODO: this is work in progress. We aim to replace the original Go |
| // implementation, which is not ideal for CUE. |
| |
| if len(args) > 0 && encoding.MapExtension(filepath.Ext(args[0])) != nil { |
| return []*build.Instance{l.cueFilesPackage(args)} |
| } |
| |
| a := []*build.Instance{} |
| for _, m := range l.importPaths(args) { |
| if m.Err != nil { |
| inst := c.newErrInstance(token.NoPos, "", m.Err) |
| a = append(a, inst) |
| continue |
| } |
| a = append(a, m.Pkgs...) |
| } |
| return a |
| } |
| |
| // Mode flags for loadImport and download (in get.go). |
| const ( |
| // resolveImport means that loadImport should do import path expansion. |
| // That is, resolveImport means that the import path came from |
| // a source file and has not been expanded yet to account for |
| // vendoring or possible module adjustment. |
| // Every import path should be loaded initially with resolveImport, |
| // and then the expanded version (for example with the /vendor/ in it) |
| // gets recorded as the canonical import path. At that point, future loads |
| // of that package must not pass resolveImport, because |
| // disallowVendor will reject direct use of paths containing /vendor/. |
| resolveImport = 1 << iota |
| ) |
| |
| type loader struct { |
| cfg *Config |
| stk importStack |
| } |
| |
| func (l *loader) abs(filename string) string { |
| if !isLocalImport(filename) { |
| return filename |
| } |
| return filepath.Join(l.cfg.Dir, filename) |
| } |
| |
| // cueFilesPackage creates a package for building a collection of CUE files |
| // (typically named on the command line). |
| func (l *loader) cueFilesPackage(files []string) *build.Instance { |
| pos := token.NoPos |
| cfg := l.cfg |
| cfg.filesMode = true |
| // ModInit() // TODO: support modules |
| pkg := l.cfg.Context.NewInstance(cfg.Dir, l.loadFunc()) |
| |
| for _, f := range files { |
| if cfg.isDir(f) { |
| return cfg.newErrInstance(token.NoPos, toImportPath(f), |
| errors.Newf(pos, "cannot mix files with directories %v", f)) |
| } |
| ext := filepath.Ext(f) |
| enc := encoding.MapExtension(ext) |
| if enc == nil { |
| return cfg.newErrInstance(token.NoPos, toImportPath(f), |
| errors.Newf(pos, "unrecognized extension %q", ext)) |
| } |
| } |
| |
| // TODO: add fields directly? |
| fp := newFileProcessor(cfg, pkg) |
| for _, file := range files { |
| path := file |
| if !filepath.IsAbs(file) { |
| path = filepath.Join(cfg.Dir, file) |
| } |
| fi, err := cfg.fileSystem.stat(path) |
| if err != nil { |
| return cfg.newErrInstance(pos, toImportPath(path), |
| errors.Wrapf(err, pos, "could not find dir %s", path)) |
| } |
| if fi.IsDir() { |
| return cfg.newErrInstance(pos, toImportPath(path), |
| errors.Newf(pos, "%s is a directory, should be a CUE file", file)) |
| } |
| fp.add(pos, cfg.Dir, file, allowAnonymous) |
| } |
| |
| // TODO: ModImportFromFiles(files) |
| _, err := filepath.Abs(cfg.Dir) |
| if err != nil { |
| return cfg.newErrInstance(pos, toImportPath(cfg.Dir), |
| errors.Wrapf(err, pos, "could convert '%s' to absolute path", cfg.Dir)) |
| } |
| pkg.Dir = cfg.Dir |
| rewriteFiles(pkg, pkg.Dir, true) |
| for _, err := range errors.Errors(fp.finalize()) { // ImportDir(&ctxt, dir, 0) |
| pkg.ReportError(err) |
| } |
| // TODO: Support module importing. |
| // if ModDirImportPath != nil { |
| // // Use the effective import path of the directory |
| // // for deciding visibility during pkg.load. |
| // bp.ImportPath = ModDirImportPath(dir) |
| // } |
| |
| for _, f := range pkg.CUEFiles { |
| fs := &l.cfg.fileSystem |
| if !fs.isAbsPath(f) { |
| f = fs.joinPath(cfg.Dir, f) |
| } |
| r, err := fs.openFile(f) |
| if err != nil { |
| pkg.ReportError(err) |
| } |
| _ = pkg.AddFile(f, r) |
| } |
| |
| pkg.Local = true |
| l.stk.Push("user") |
| pkg.Complete() |
| l.stk.Pop() |
| pkg.Local = true |
| //pkg.LocalPrefix = dirToImportPath(dir) |
| pkg.DisplayPath = "command-line-arguments" |
| pkg.Match = files |
| |
| return pkg |
| } |
| |
| func cleanImport(path string) string { |
| orig := path |
| path = pathpkg.Clean(path) |
| if strings.HasPrefix(orig, "./") && path != ".." && !strings.HasPrefix(path, "../") { |
| path = "./" + path |
| } |
| return path |
| } |
| |
| // An importStack is a stack of import paths, possibly with the suffix " (test)" appended. |
| // The import path of a test package is the import path of the corresponding |
| // non-test package with the suffix "_test" added. |
| type importStack []string |
| |
| func (s *importStack) Push(p string) { |
| *s = append(*s, p) |
| } |
| |
| func (s *importStack) Pop() { |
| *s = (*s)[0 : len(*s)-1] |
| } |
| |
| func (s *importStack) Copy() []string { |
| return append([]string{}, *s...) |
| } |
| |
| // shorterThan reports whether sp is shorter than t. |
| // We use this to record the shortest import sequences |
| // that leads to a particular package. |
| func (sp *importStack) shorterThan(t []string) bool { |
| s := *sp |
| if len(s) != len(t) { |
| return len(s) < len(t) |
| } |
| // If they are the same length, settle ties using string ordering. |
| for i := range s { |
| if s[i] != t[i] { |
| return s[i] < t[i] |
| } |
| } |
| return false // they are equal |
| } |
| |
| // reusePackage reuses package p to satisfy the import at the top |
| // of the import stack stk. If this use causes an import loop, |
| // reusePackage updates p's error information to record the loop. |
| func (l *loader) reusePackage(p *build.Instance) *build.Instance { |
| // We use p.Internal.Imports==nil to detect a package that |
| // is in the midst of its own loadPackage call |
| // (all the recursion below happens before p.Internal.Imports gets set). |
| if p.ImportPaths == nil { |
| if err := lastError(p); err == nil { |
| err = l.errPkgf(nil, "import cycle not allowed") |
| err.IsImportCycle = true |
| report(p, err) |
| } |
| p.Incomplete = true |
| } |
| // Don't rewrite the import stack in the error if we have an import cycle. |
| // If we do, we'll lose the path that describes the cycle. |
| if err := lastError(p); err != nil && !err.IsImportCycle && l.stk.shorterThan(err.ImportStack) { |
| err.ImportStack = l.stk.Copy() |
| } |
| return p |
| } |
| |
| // dirToImportPath returns the pseudo-import path we use for a package |
| // outside the CUE path. It begins with _/ and then contains the full path |
| // to the directory. If the package lives in c:\home\gopher\my\pkg then |
| // the pseudo-import path is _/c_/home/gopher/my/pkg. |
| // Using a pseudo-import path like this makes the ./ imports no longer |
| // a special case, so that all the code to deal with ordinary imports works |
| // automatically. |
| func dirToImportPath(dir string) string { |
| return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir))) |
| } |
| |
| func makeImportValid(r rune) rune { |
| // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport. |
| const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD" |
| if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) { |
| return '_' |
| } |
| return r |
| } |