| // Copyright 2020 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 cmd |
| |
| import ( |
| "fmt" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/ast/astutil" |
| "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/encoding" |
| ) |
| |
| // This file contains logic for placing orphan files within a CUE namespace. |
| |
| func (b *buildPlan) placeOrphans(i *build.Instance) (ok bool, err error) { |
| var ( |
| perFile = flagFiles.Bool(b.cmd) |
| useList = flagList.Bool(b.cmd) |
| path = flagPath.StringArray(b.cmd) |
| useContext = flagWithContext.Bool(b.cmd) |
| ) |
| if !b.importing && !perFile && !useList && len(path) == 0 { |
| if useContext { |
| return false, fmt.Errorf( |
| "flag %q must be used with at least one of flag %q, %q, or %q", |
| flagWithContext, flagPath, flagList, flagFiles, |
| ) |
| } |
| // TODO: should we remove this optimization? This is really added as a |
| // conversion safety. |
| if len(i.OrphanedFiles)+len(i.BuildFiles) <= 1 || b.cfg.noMerge { |
| return false, err |
| } |
| } |
| |
| pkg := b.encConfig.PkgName |
| if pkg == "" { |
| pkg = i.PkgName |
| } else if pkg != "" && i.PkgName != "" && i.PkgName != pkg && !flagForce.Bool(b.cmd) { |
| return false, fmt.Errorf( |
| "%q flag clashes with existing package name (%s vs %s)", |
| flagPackage, pkg, i.PkgName, |
| ) |
| } |
| |
| var files []*ast.File |
| |
| re, err := regexp.Compile(b.cfg.fileFilter) |
| if err != nil { |
| return false, err |
| } |
| |
| for _, f := range i.OrphanedFiles { |
| if !i.User && !re.MatchString(filepath.Base(f.Filename)) { |
| return false, nil |
| } |
| |
| // We add the module root to the path if there is a module defined. |
| c := *b.encConfig |
| if i.Module != "" { |
| c.ProtoPath = append(c.ProtoPath, i.Root) |
| } |
| |
| d := encoding.NewDecoder(f, &c) |
| defer d.Close() |
| |
| var objs []*ast.File |
| |
| // Filter only need to filter files that can stream: |
| for ; !d.Done(); d.Next() { |
| if f := d.File(); f != nil { |
| f.Filename = newName(d.Filename(), 0) |
| objs = append(objs, f) |
| } |
| } |
| if err := d.Err(); err != nil { |
| return false, err |
| } |
| |
| if perFile { |
| for i, obj := range objs { |
| f, err := placeOrphans(b.cmd, d.Filename(), pkg, obj) |
| if err != nil { |
| return false, err |
| } |
| f.Filename = newName(d.Filename(), i) |
| files = append(files, f) |
| } |
| continue |
| } |
| // TODO: consider getting rid of this requirement. It is important that |
| // import will catch conflicts ahead of time then, though, and report |
| // this messages as a possible solution if there are conflicts. |
| if b.importing && len(objs) > 1 && len(path) == 0 && !useList { |
| return false, fmt.Errorf( |
| "%s, %s, or %s flag needed to handle multiple objects in file %s", |
| flagPath, flagList, flagFiles, shortFile(i.Root, f)) |
| } |
| |
| if !useList && len(path) == 0 && !useContext { |
| for _, f := range objs { |
| if pkg := c.PkgName; pkg != "" { |
| internal.SetPackage(f, pkg, false) |
| } |
| files = append(files, f) |
| } |
| } else { |
| // TODO: handle imports correctly, i.e. for proto. |
| f, err := placeOrphans(b.cmd, d.Filename(), pkg, objs...) |
| if err != nil { |
| return false, err |
| } |
| f.Filename = newName(d.Filename(), 0) |
| files = append(files, f) |
| } |
| } |
| |
| b.imported = append(b.imported, files...) |
| for _, f := range files { |
| if err := i.AddSyntax(f); err != nil { |
| return false, err |
| } |
| i.BuildFiles = append(i.BuildFiles, &build.File{ |
| Filename: f.Filename, |
| Encoding: build.CUE, |
| Source: f, |
| }) |
| } |
| return true, nil |
| } |
| |
| func toExpr(f *ast.File) (expr ast.Expr, pkg *ast.Package) { |
| p := len(f.Preamble()) |
| return &ast.StructLit{Elts: f.Decls[p:]}, pkg |
| } |
| |
| func placeOrphans(cmd *Command, filename, pkg string, objs ...*ast.File) (*ast.File, error) { |
| f := &ast.File{} |
| |
| index := newIndex() |
| for i, file := range objs { |
| if i == 0 { |
| astutil.CopyMeta(f, file) |
| } |
| expr, p := toExpr(file) |
| |
| var pathElems []ast.Label |
| var pathTokens []token.Token |
| |
| switch { |
| case len(flagPath.StringArray(cmd)) > 0: |
| expr := expr |
| if flagWithContext.Bool(cmd) { |
| expr = ast.NewStruct( |
| "data", expr, |
| "filename", ast.NewString(filename), |
| "index", ast.NewLit(token.INT, strconv.Itoa(i)), |
| "recordCount", ast.NewLit(token.INT, strconv.Itoa(len(objs))), |
| ) |
| } |
| var f *ast.File |
| if s, ok := expr.(*ast.StructLit); ok { |
| f = &ast.File{Decls: s.Elts} |
| } else { |
| f = &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: expr}}} |
| } |
| err := astutil.Sanitize(f) |
| if err != nil { |
| return nil, errors.Wrapf(err, token.NoPos, |
| "invalid combination of input files") |
| } |
| inst, err := runtime.CompileFile(f) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, str := range flagPath.StringArray(cmd) { |
| l, err := parser.ParseExpr("--path", str) |
| if err != nil { |
| labels, tokens, err := parseFullPath(inst, str) |
| if err != nil { |
| return nil, fmt.Errorf( |
| `labels must be expressions (-l foo -l 'strings.ToLower(bar)') or full paths (-l '"foo": "\(strings.ToLower(bar))":) : %v`, err) |
| } |
| pathElems = append(pathElems, labels...) |
| pathTokens = append(pathTokens, tokens...) |
| continue |
| } |
| |
| str, err := inst.Eval(l).String() |
| if err != nil { |
| return nil, fmt.Errorf("unsupported label path type: %v", err) |
| } |
| pathElems = append(pathElems, ast.NewString(str)) |
| pathTokens = append(pathTokens, 0) |
| } |
| } |
| |
| if flagList.Bool(cmd) { |
| idx := index |
| for _, e := range pathElems { |
| idx = idx.label(e) |
| } |
| if idx.field.Value == nil { |
| idx.field.Value = &ast.ListLit{ |
| Lbrack: token.NoSpace.Pos(), |
| Rbrack: token.NoSpace.Pos(), |
| } |
| } |
| list := idx.field.Value.(*ast.ListLit) |
| list.Elts = append(list.Elts, expr) |
| } else if len(pathElems) == 0 { |
| obj, ok := expr.(*ast.StructLit) |
| if !ok { |
| if _, ok := expr.(*ast.ListLit); ok { |
| return nil, fmt.Errorf("expected struct as object root, did you mean to use the --list flag?") |
| } |
| return nil, fmt.Errorf("cannot map non-struct to object root") |
| } |
| f.Decls = append(f.Decls, obj.Elts...) |
| } else { |
| field := &ast.Field{Label: pathElems[0]} |
| field.Token = pathTokens[0] |
| f.Decls = append(f.Decls, field) |
| if p != nil { |
| astutil.CopyComments(field, p) |
| } |
| for i, e := range pathElems[1:] { |
| newField := &ast.Field{Label: e} |
| newVal := ast.NewStruct(newField) |
| newField.Token = pathTokens[i+1] |
| field.Value = newVal |
| field = newField |
| } |
| field.Value = expr |
| } |
| } |
| |
| if pkg != "" { |
| internal.SetPackage(f, pkg, false) |
| } |
| |
| if flagList.Bool(cmd) { |
| switch x := index.field.Value.(type) { |
| case *ast.StructLit: |
| f.Decls = append(f.Decls, x.Elts...) |
| case *ast.ListLit: |
| f.Decls = append(f.Decls, &ast.EmbedDecl{Expr: x}) |
| default: |
| panic("unreachable") |
| } |
| } |
| |
| return f, astutil.Sanitize(f) |
| } |
| |
| func parseFullPath(inst *cue.Instance, exprs string) (p []ast.Label, t []token.Token, err error) { |
| f, err := parser.ParseFile("--path", exprs+"_") |
| if err != nil { |
| return nil, nil, fmt.Errorf("parser error in path %q: %v", exprs, err) |
| } |
| |
| if len(f.Decls) != 1 { |
| return nil, nil, errors.New("path flag must be a space-separated sequence of labels") |
| } |
| |
| for d := f.Decls[0]; ; { |
| field, ok := d.(*ast.Field) |
| if !ok { |
| // This should never happen |
| return nil, nil, errors.New("%q not a sequence of labels") |
| } |
| |
| t = append(t, field.Token) |
| |
| switch x := field.Label.(type) { |
| case *ast.Ident, *ast.BasicLit: |
| p = append(p, x) |
| |
| case *ast.TemplateLabel: |
| return nil, nil, fmt.Errorf("template labels not supported in path flag") |
| |
| case ast.Expr: |
| v := inst.Eval(x) |
| if v.Kind() == cue.BottomKind { |
| return nil, nil, v.Err() |
| } |
| p = append(p, v.Syntax().(ast.Label)) |
| |
| } |
| |
| v, ok := field.Value.(*ast.StructLit) |
| if !ok { |
| break |
| } |
| |
| if len(v.Elts) != 1 { |
| return nil, nil, errors.New("path value may not contain a struct") |
| } |
| |
| d = v.Elts[0] |
| } |
| return p, t, nil |
| } |
| |
| type listIndex struct { |
| index map[string]*listIndex |
| field *ast.Field |
| } |
| |
| func newIndex() *listIndex { |
| return &listIndex{ |
| index: map[string]*listIndex{}, |
| field: &ast.Field{}, |
| } |
| } |
| |
| func (x *listIndex) label(label ast.Label) *listIndex { |
| key := internal.DebugStr(label) |
| idx := x.index[key] |
| if idx == nil { |
| if x.field.Value == nil { |
| x.field.Value = &ast.StructLit{} |
| } |
| obj := x.field.Value.(*ast.StructLit) |
| newField := &ast.Field{Label: label} |
| obj.Elts = append(obj.Elts, newField) |
| idx = &listIndex{ |
| index: map[string]*listIndex{}, |
| field: newField, |
| } |
| x.index[key] = idx |
| } |
| return idx |
| } |
| |
| func newName(filename string, i int) string { |
| if filename == "-" { |
| return filename |
| } |
| ext := filepath.Ext(filename) |
| filename = filename[:len(filename)-len(ext)] |
| if i > 0 { |
| filename += fmt.Sprintf("-%d", i) |
| } |
| filename += ".cue" |
| return filename |
| } |