blob: 0d2d35c93cd1afe61fe56961bea40389100f6256 [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
17import (
18 "bytes"
19 "fmt"
20 "log"
21 pathpkg "path"
22 "path/filepath"
23 "sort"
24 "strconv"
25 "strings"
26 "unicode"
27 "unicode/utf8"
28
29 "cuelang.org/go/cue/ast"
30 build "cuelang.org/go/cue/build"
31 "cuelang.org/go/cue/encoding"
32 "cuelang.org/go/cue/parser"
33 "cuelang.org/go/cue/token"
34)
35
36// An importMode controls the behavior of the Import method.
37type importMode uint
38
39const (
40 // If findOnly is set, Import stops after locating the directory
41 // that should contain the sources for a package. It does not
42 // read any files in the directory.
43 findOnly importMode = 1 << iota
44
45 // If importComment is set, parse import comments on package statements.
46 // Import returns an error if it finds a comment it cannot understand
47 // or finds conflicting comments in multiple source files.
48 // See golang.org/s/go14customimport for more information.
49 importComment
50
51 allowAnonymous
52)
53
54// importPkg returns details about the CUE package named by the import path,
55// interpreting local import paths relative to the srcDir directory.
56// If the path is a local import path naming a package that can be imported
57// using a standard import path, the returned package will set p.ImportPath
58// to that path.
59//
60// In the directory and ancestor directories up to including one with a
61// cue.mod file, all .cue files are considered part of the package except for:
62//
63// - files starting with _ or . (likely editor temporary files)
64// - files with build constraints not satisfied by the context
65//
66// If an error occurs, importPkg sets the error in the returned instance,
67// which then may contain partial information.
68//
69func (l *loader) importPkg(path, srcDir string) *build.Instance {
70 l.stk.Push(path)
71 defer l.stk.Pop()
72
73 cfg := l.cfg
74 ctxt := &cfg.fileSystem
75
76 parentPath := path
77 if isLocalImport(path) {
78 parentPath = filepath.Join(srcDir, path)
79 }
80 p := cfg.Context.NewInstance(path, l.loadFunc(parentPath))
81 p.DisplayPath = path
82
83 isLocal := isLocalImport(path)
84 var modDir string
85 // var modErr error
86 if !isLocal {
87 // TODO(mpvl): support module lookup
88 }
89
90 p.Local = isLocal
91
92 if err := updateDirs(cfg, p, path, srcDir, 0); err != nil {
93 p.ReportError(err)
94 return p
95 }
96
97 if modDir == "" && path != cleanImport(path) {
98 report(p, l.errPkgf(nil,
99 "non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
100 p.Incomplete = true
101 return p
102 }
103
104 fp := newFileProcessor(cfg, p)
105
106 root := p.Dir
107
108 for dir := p.Dir; ctxt.isDir(dir); {
109 files, err := ctxt.readDir(dir)
110 if err != nil {
111 p.ReportError(err)
112 return p
113 }
114 rootFound := false
115 for _, f := range files {
116 if f.IsDir() {
117 continue
118 }
119 if fp.add(dir, f.Name(), importComment) {
120 root = dir
121 }
122 if f.Name() == "cue.mod" {
123 root = dir
124 rootFound = true
125 }
126 }
127
128 if rootFound || dir == p.Root || fp.pkg.PkgName == "" {
129 break
130 }
131
132 // From now on we just ignore files that do not belong to the same
133 // package.
134 fp.ignoreOther = true
135
136 parent, _ := filepath.Split(filepath.Clean(dir))
137 if parent == dir {
138 break
139 }
140 dir = parent
141 }
142
143 rewriteFiles(p, root, false)
144 if err := fp.finalize(); err != nil {
145 p.ReportError(err)
146 return p
147 }
148
149 for _, f := range p.CUEFiles {
150 if !filepath.IsAbs(f) {
151 f = filepath.Join(root, f)
152 }
153 p.AddFile(f, nil)
154 }
155 p.Complete()
156 return p
157}
158
159// loadFunc creates a LoadFunc that can be used to create new build.Instances.
160func (l *loader) loadFunc(parentPath string) build.LoadFunc {
161
162 return func(path string) *build.Instance {
163 cfg := l.cfg
164
165 // TODO: HACK: for now we don't handle any imports that are not
166 // relative paths.
167 if !isLocalImport(path) {
168 return nil
169 }
170
171 if strings.Contains(path, "@") {
172 i := cfg.newInstance(path)
173 report(i, l.errPkgf(nil,
174 "can only use path@version syntax with 'cue get'"))
175 return i
176 }
177
178 return l.importPkg(path, parentPath)
179 }
180}
181
182func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) error {
183 ctxt := &c.fileSystem
184 // path := p.ImportPath
185 if path == "" {
186 return fmt.Errorf("import %q: invalid import path", path)
187 }
188
189 if isLocalImport(path) {
190 if srcDir == "" {
191 return fmt.Errorf("import %q: import relative to unknown directory", path)
192 }
193 if !ctxt.isAbsPath(path) {
194 p.Dir = ctxt.joinPath(srcDir, path)
195 }
196 return nil
197 }
198
199 if strings.HasPrefix(path, "/") {
200 return fmt.Errorf("import %q: cannot import absolute path", path)
201 }
202
203 // TODO: Lookup the import in dir "pkg" at the module root.
204
205 // package was not found
206 return fmt.Errorf("cannot find package %q", path)
207}
208
209func normPrefix(root, path string, isLocal bool) string {
210 root = filepath.Clean(root)
211 prefix := ""
212 if isLocal {
213 prefix = "." + string(filepath.Separator)
214 }
215 if !strings.HasSuffix(root, string(filepath.Separator)) &&
216 strings.HasPrefix(path, root) {
217 path = prefix + path[len(root)+1:]
218 }
219 return path
220}
221
222func rewriteFiles(p *build.Instance, root string, isLocal bool) {
223 p.Root = root
224 for i, path := range p.CUEFiles {
225 p.CUEFiles[i] = normPrefix(root, path, isLocal)
226 sortParentsFirst(p.CUEFiles)
227 }
228 for i, path := range p.TestCUEFiles {
229 p.TestCUEFiles[i] = normPrefix(root, path, isLocal)
230 sortParentsFirst(p.TestCUEFiles)
231 }
232 for i, path := range p.ToolCUEFiles {
233 p.ToolCUEFiles[i] = normPrefix(root, path, isLocal)
234 sortParentsFirst(p.ToolCUEFiles)
235 }
236 for i, path := range p.IgnoredCUEFiles {
237 if strings.HasPrefix(path, root) {
238 p.IgnoredCUEFiles[i] = normPrefix(root, path, isLocal)
239 }
240 }
241 for i, path := range p.InvalidCUEFiles {
242 p.InvalidCUEFiles[i] = normPrefix(root, path, isLocal)
243 sortParentsFirst(p.InvalidCUEFiles)
244 }
245}
246
247func sortParentsFirst(s []string) {
248 sort.Slice(s, func(i, j int) bool {
249 return len(filepath.Dir(s[i])) < len(filepath.Dir(s[j]))
250 })
251}
252
253type fileProcessor struct {
254 firstFile string
255 firstCommentFile string
256 imported map[string][]token.Position
257 allTags map[string]bool
258 allFiles bool
259 ignoreOther bool // ignore files from other packages
260
261 c *Config
262 pkg *build.Instance
263
264 err error
265}
266
267func newFileProcessor(c *Config, p *build.Instance) *fileProcessor {
268 return &fileProcessor{
269 imported: make(map[string][]token.Position),
270 allTags: make(map[string]bool),
271 c: c,
272 pkg: p,
273 }
274}
275
276func (fp *fileProcessor) finalize() error {
277 p := fp.pkg
278 if fp.err != nil {
279 return fp.err
280 }
281 if len(p.CUEFiles) == 0 && !fp.c.DataFiles {
282 return &noCUEError{Package: p, Dir: p.Dir, Ignored: len(p.IgnoredCUEFiles) > 0}
283 }
284
285 for tag := range fp.allTags {
286 p.AllTags = append(p.AllTags, tag)
287 }
288 sort.Strings(p.AllTags)
289
290 p.ImportPaths, _ = cleanImports(fp.imported)
291
292 return nil
293}
294
295func (fp *fileProcessor) add(root, path string, mode importMode) (added bool) {
296 fullPath := path
297 if !filepath.IsAbs(path) {
298 fullPath = filepath.Join(root, path)
299 }
300 name := filepath.Base(fullPath)
301 dir := filepath.Dir(fullPath)
302
303 fset := token.NewFileSet()
304 ext := nameExt(name)
305 p := fp.pkg
306
307 badFile := func(err error) bool {
308 if fp.err == nil {
309 fp.err = err
310 }
311 p.InvalidCUEFiles = append(p.InvalidCUEFiles, fullPath)
312 return true
313 }
314
315 match, data, filename, err := matchFile(fp.c, dir, name, true, fp.allFiles, fp.allTags)
316 if err != nil {
317 return badFile(err)
318 }
319 if !match {
320 if ext == cueSuffix {
321 p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
322 } else if encoding.MapExtension(ext) != nil {
323 p.DataFiles = append(p.DataFiles, fullPath)
324 }
325 return false // don't mark as added
326 }
327
328 pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly, parser.ParseComments)
329 if err != nil {
330 return badFile(err)
331 }
332
333 pkg := ""
334 if pf.Name != nil {
335 pkg = pf.Name.Name
336 }
337 if pkg == "" && mode&allowAnonymous == 0 {
338 p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
339 return false // don't mark as added
340 }
341
342 if fp.c.Package != "" {
343 if pkg != fp.c.Package {
344 if fp.ignoreOther {
345 p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
346 return false
347 }
348 // TODO: package does not conform with requested.
349 return badFile(fmt.Errorf("%s: found package %q; want %q", filename, pkg, fp.c.Package))
350 }
351 } else if fp.firstFile == "" {
352 p.PkgName = pkg
353 fp.firstFile = name
354 } else if pkg != p.PkgName {
355 if fp.ignoreOther {
356 p.IgnoredCUEFiles = append(p.IgnoredCUEFiles, fullPath)
357 return false
358 }
359 return badFile(&multiplePackageError{
360 Dir: p.Dir,
361 Packages: []string{p.PkgName, pkg},
362 Files: []string{fp.firstFile, name},
363 })
364 }
365
366 isTest := strings.HasSuffix(name, "_test"+cueSuffix)
367 isTool := strings.HasSuffix(name, "_tool"+cueSuffix)
368
369 if mode&importComment != 0 {
370 qcom, line := findimportComment(data)
371 if line != 0 {
372 com, err := strconv.Unquote(qcom)
373 if err != nil {
374 badFile(fmt.Errorf("%s:%d: cannot parse import comment", filename, line))
375 } else if p.ImportComment == "" {
376 p.ImportComment = com
377 fp.firstCommentFile = name
378 } else if p.ImportComment != com {
379 badFile(fmt.Errorf("found import comments %q (%s) and %q (%s) in %s", p.ImportComment, fp.firstCommentFile, com, name, p.Dir))
380 }
381 }
382 }
383
384 for _, decl := range pf.Decls {
385 d, ok := decl.(*ast.ImportDecl)
386 if !ok {
387 continue
388 }
389 for _, spec := range d.Specs {
390 quoted := spec.Path.Value
391 path, err := strconv.Unquote(quoted)
392 if err != nil {
393 log.Panicf("%s: parser returned invalid quoted string: <%s>", filename, quoted)
394 }
395 if !isTest || fp.c.Tests {
396 fp.imported[path] = append(fp.imported[path], fset.Position(spec.Pos()))
397 }
398 }
399 }
400 switch {
401 case isTest:
402 p.TestCUEFiles = append(p.TestCUEFiles, fullPath)
403 case isTool:
Marcel van Lohuizenf23fbff2018-12-20 12:23:16 +0100404 p.ToolCUEFiles = append(p.ToolCUEFiles, fullPath)
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +0100405 default:
406 p.CUEFiles = append(p.CUEFiles, fullPath)
407 }
408 return true
409}
410
411func nameExt(name string) string {
412 i := strings.LastIndex(name, ".")
413 if i < 0 {
414 return ""
415 }
416 return name[i:]
417}
418
419// hasCUEFiles reports whether dir contains any files with names ending in .go.
420// For a vendor check we must exclude directories that contain no .go files.
421// Otherwise it is not possible to vendor just a/b/c and still import the
422// non-vendored a/b. See golang.org/issue/13832.
423func hasCUEFiles(ctxt *fileSystem, dir string) bool {
424 ents, _ := ctxt.readDir(dir)
425 for _, ent := range ents {
426 if !ent.IsDir() && strings.HasSuffix(ent.Name(), cueSuffix) {
427 return true
428 }
429 }
430 return false
431}
432
433func findimportComment(data []byte) (s string, line int) {
434 // expect keyword package
435 word, data := parseWord(data)
436 if string(word) != "package" {
437 return "", 0
438 }
439
440 // expect package name
441 _, data = parseWord(data)
442
443 // now ready for import comment, a // or /* */ comment
444 // beginning and ending on the current line.
445 for len(data) > 0 && (data[0] == ' ' || data[0] == '\t' || data[0] == '\r') {
446 data = data[1:]
447 }
448
449 var comment []byte
450 switch {
451 case bytes.HasPrefix(data, slashSlash):
452 i := bytes.Index(data, newline)
453 if i < 0 {
454 i = len(data)
455 }
456 comment = data[2:i]
457 case bytes.HasPrefix(data, slashStar):
458 data = data[2:]
459 i := bytes.Index(data, starSlash)
460 if i < 0 {
461 // malformed comment
462 return "", 0
463 }
464 comment = data[:i]
465 if bytes.Contains(comment, newline) {
466 return "", 0
467 }
468 }
469 comment = bytes.TrimSpace(comment)
470
471 // split comment into `import`, `"pkg"`
472 word, arg := parseWord(comment)
473 if string(word) != "import" {
474 return "", 0
475 }
476
477 line = 1 + bytes.Count(data[:cap(data)-cap(arg)], newline)
478 return strings.TrimSpace(string(arg)), line
479}
480
481var (
482 slashSlash = []byte("//")
483 slashStar = []byte("/*")
484 starSlash = []byte("*/")
485 newline = []byte("\n")
486)
487
488// skipSpaceOrComment returns data with any leading spaces or comments removed.
489func skipSpaceOrComment(data []byte) []byte {
490 for len(data) > 0 {
491 switch data[0] {
492 case ' ', '\t', '\r', '\n':
493 data = data[1:]
494 continue
495 case '/':
496 if bytes.HasPrefix(data, slashSlash) {
497 i := bytes.Index(data, newline)
498 if i < 0 {
499 return nil
500 }
501 data = data[i+1:]
502 continue
503 }
504 if bytes.HasPrefix(data, slashStar) {
505 data = data[2:]
506 i := bytes.Index(data, starSlash)
507 if i < 0 {
508 return nil
509 }
510 data = data[i+2:]
511 continue
512 }
513 }
514 break
515 }
516 return data
517}
518
519// parseWord skips any leading spaces or comments in data
520// and then parses the beginning of data as an identifier or keyword,
521// returning that word and what remains after the word.
522func parseWord(data []byte) (word, rest []byte) {
523 data = skipSpaceOrComment(data)
524
525 // Parse past leading word characters.
526 rest = data
527 for {
528 r, size := utf8.DecodeRune(rest)
529 if unicode.IsLetter(r) || '0' <= r && r <= '9' || r == '_' {
530 rest = rest[size:]
531 continue
532 }
533 break
534 }
535
536 word = data[:len(data)-len(rest)]
537 if len(word) == 0 {
538 return nil, nil
539 }
540
541 return word, rest
542}
543
544func cleanImports(m map[string][]token.Position) ([]string, map[string][]token.Position) {
545 all := make([]string, 0, len(m))
546 for path := range m {
547 all = append(all, path)
548 }
549 sort.Strings(all)
550 return all, m
551}
552
553// // Import is shorthand for Default.Import.
554// func Import(path, srcDir string, mode ImportMode) (*Package, error) {
555// return Default.Import(path, srcDir, mode)
556// }
557
558// // ImportDir is shorthand for Default.ImportDir.
559// func ImportDir(dir string, mode ImportMode) (*Package, error) {
560// return Default.ImportDir(dir, mode)
561// }
562
563var slashslash = []byte("//")
564
565// isLocalImport reports whether the import path is
566// a local import path, like ".", "..", "./foo", or "../foo".
567func isLocalImport(path string) bool {
568 return path == "." || path == ".." ||
569 strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
570}