blob: 670bf30a2cfe7a8e8a634c9f8fefe7b608bfe0be [file] [log] [blame]
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +01001// Copyright 2018 The CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package load
16
17// Files in package are to a large extent based on Go files from the following
18// Go packages:
19// - cmd/go/internal/load
20// - go/build
21
22import (
23 "errors"
24 "fmt"
25 "os"
26 pathpkg "path"
27 "path/filepath"
28 "strings"
29 "unicode"
30
31 build "cuelang.org/go/cue/build"
32 "cuelang.org/go/cue/token"
33)
34
35// Instances returns the instances named by the command line arguments 'args'.
36// If errors occur trying to load an instance it is returned with Incomplete
37// set. Errors directly related to loading the instance are recorded in this
38// instance, but errors that occur loading dependencies are recorded in these
39// dependencies.
40func Instances(args []string, c *Config) []*build.Instance {
41 if c == nil {
42 c = &Config{}
43 }
44
45 c, err := c.complete()
46 if err != nil {
47 return nil
48 }
49
50 l := c.loader
51
52 if len(args) > 0 && strings.HasSuffix(args[0], cueSuffix) {
53 return []*build.Instance{l.cueFilesPackage(args)}
54 }
55
56 dummy := c.newInstance("user")
57 dummy.Local = true
58
59 a := []*build.Instance{}
60 for _, m := range l.importPaths(args) {
61 if m.Err != nil {
62 inst := c.newErrInstance(m, "", m.Err)
63 a = append(a, inst)
64 continue
65 }
66 a = append(a, m.Pkgs...)
67 }
68 return a
69}
70
71// Mode flags for loadImport and download (in get.go).
72const (
73 // resolveImport means that loadImport should do import path expansion.
74 // That is, resolveImport means that the import path came from
75 // a source file and has not been expanded yet to account for
76 // vendoring or possible module adjustment.
77 // Every import path should be loaded initially with resolveImport,
78 // and then the expanded version (for example with the /vendor/ in it)
79 // gets recorded as the canonical import path. At that point, future loads
80 // of that package must not pass resolveImport, because
81 // disallowVendor will reject direct use of paths containing /vendor/.
82 resolveImport = 1 << iota
83
84 // resolveModule is for download (part of "go get") and indicates
85 // that the module adjustment should be done, but not vendor adjustment.
86 resolveModule
87
88 // getTestDeps is for download (part of "go get") and indicates
89 // that test dependencies should be fetched too.
90 getTestDeps
91)
92
93func firstPos(p []token.Position) token.Position {
94 if len(p) == 0 {
95 return token.Position{}
96 }
97 return p[0]
98}
99
100type loader struct {
101 cfg *Config
102 stk importStack
103}
104
105func (l *loader) abs(filename string) string {
106 if !isLocalImport(filename) {
107 return filename
108 }
109 return filepath.Join(l.cfg.Dir, filename)
110}
111
112// cueFilesPackage creates a package for building a collection of CUE files
113// (typically named on the command line).
114func (l *loader) cueFilesPackage(files []string) *build.Instance {
115 cfg := l.cfg
116 // ModInit() // TODO: support modules
117 for _, f := range files {
118 if !strings.HasSuffix(f, ".cue") {
119 return cfg.newErrInstance(nil, f,
120 errors.New("named files must be .cue files"))
121 }
122 }
123
124 pkg := l.cfg.Context.NewInstance(cfg.Dir, l.loadFunc(cfg.Dir))
125 // TODO: add fiels directly?
126 fp := newFileProcessor(cfg, pkg)
127 for _, file := range files {
128 path := file
129 if !filepath.IsAbs(file) {
130 path = filepath.Join(cfg.Dir, file)
131 }
132 fi, err := os.Stat(path)
133 if err != nil {
134 return cfg.newErrInstance(nil, path, err)
135 }
136 if fi.IsDir() {
137 return cfg.newErrInstance(nil, path,
138 fmt.Errorf("%s is a directory, should be a CUE file", file))
139 }
140 fp.add(cfg.Dir, file, allowAnonymous)
141 }
142
143 // TODO: ModImportFromFiles(files)
144 _, err := filepath.Abs(cfg.Dir)
145 if err != nil {
146 return cfg.newErrInstance(nil, cfg.Dir, err)
147 }
148 pkg.Dir = cfg.Dir
149 rewriteFiles(pkg, pkg.Dir, true)
150 err = fp.finalize() // ImportDir(&ctxt, dir, 0)
151 // TODO: Support module importing.
152 // if ModDirImportPath != nil {
153 // // Use the effective import path of the directory
154 // // for deciding visibility during pkg.load.
155 // bp.ImportPath = ModDirImportPath(dir)
156 // }
157
158 for _, f := range pkg.CUEFiles {
159 if !filepath.IsAbs(f) {
160 f = filepath.Join(cfg.Dir, f)
161 }
162 pkg.AddFile(f, nil)
163 }
164
165 pkg.Local = true
166 l.stk.Push("user")
167 pkg.Complete()
168 l.stk.Pop()
169 pkg.Local = true
170 //pkg.LocalPrefix = dirToImportPath(dir)
171 pkg.DisplayPath = "command-line-arguments"
172 pkg.Match = files
173
174 return pkg
175}
176
177func cleanImport(path string) string {
178 orig := path
179 path = pathpkg.Clean(path)
180 if strings.HasPrefix(orig, "./") && path != ".." && !strings.HasPrefix(path, "../") {
181 path = "./" + path
182 }
183 return path
184}
185
186// An importStack is a stack of import paths, possibly with the suffix " (test)" appended.
187// The import path of a test package is the import path of the corresponding
188// non-test package with the suffix "_test" added.
189type importStack []string
190
191func (s *importStack) Push(p string) {
192 *s = append(*s, p)
193}
194
195func (s *importStack) Pop() {
196 *s = (*s)[0 : len(*s)-1]
197}
198
199func (s *importStack) Copy() []string {
200 return append([]string{}, *s...)
201}
202
203// shorterThan reports whether sp is shorter than t.
204// We use this to record the shortest import sequences
205// that leads to a particular package.
206func (sp *importStack) shorterThan(t []string) bool {
207 s := *sp
208 if len(s) != len(t) {
209 return len(s) < len(t)
210 }
211 // If they are the same length, settle ties using string ordering.
212 for i := range s {
213 if s[i] != t[i] {
214 return s[i] < t[i]
215 }
216 }
217 return false // they are equal
218}
219
220// reusePackage reuses package p to satisfy the import at the top
221// of the import stack stk. If this use causes an import loop,
222// reusePackage updates p's error information to record the loop.
223func (l *loader) reusePackage(p *build.Instance) *build.Instance {
224 // We use p.Internal.Imports==nil to detect a package that
225 // is in the midst of its own loadPackage call
226 // (all the recursion below happens before p.Internal.Imports gets set).
227 if p.ImportPaths == nil {
228 if err := lastError(p); err == nil {
229 err = l.errPkgf(nil, "import cycle not allowed")
230 err.IsImportCycle = true
231 report(p, err)
232 }
233 p.Incomplete = true
234 }
235 // Don't rewrite the import stack in the error if we have an import cycle.
236 // If we do, we'll lose the path that describes the cycle.
237 if err := lastError(p); err != nil && !err.IsImportCycle && l.stk.shorterThan(err.ImportStack) {
238 err.ImportStack = l.stk.Copy()
239 }
240 return p
241}
242
243// dirToImportPath returns the pseudo-import path we use for a package
244// outside the CUE path. It begins with _/ and then contains the full path
245// to the directory. If the package lives in c:\home\gopher\my\pkg then
246// the pseudo-import path is _/c_/home/gopher/my/pkg.
247// Using a pseudo-import path like this makes the ./ imports no longer
248// a special case, so that all the code to deal with ordinary imports works
249// automatically.
250func dirToImportPath(dir string) string {
251 return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
252}
253
254func makeImportValid(r rune) rune {
255 // Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
256 const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
257 if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
258 return '_'
259 }
260 return r
261}