blob: 87df949ea0e512cb10aa79137457f083acec2537 [file] [log] [blame]
// 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"
"hash/fnv"
"strings"
"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/format"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
)
func fixAll(a []*build.Instance) errors.Error {
// Collect all
p := processor{
instances: a,
done: map[ast.Node]bool{},
rename: map[*ast.Ident]string{},
ambiguous: map[string][]token.Pos{},
}
p.visitAll(func(f *ast.File) { fix(f) })
instances := cue.Build(a)
p.updateValues(instances)
p.visitAll(p.tagAmbiguous)
p.rewriteIdents()
p.visitAll(p.renameFields)
return p.err
}
type processor struct {
instances []*build.Instance
done map[ast.Node]bool
// Evidence for rewrites. Rewrite in a later pass.
rename map[*ast.Ident]string
ambiguous map[string][]token.Pos
stack []cue.Value
err errors.Error
}
func (p *processor) updateValues(instances []*cue.Instance) {
for _, inst := range instances {
inst.Value().Walk(p.visit, nil)
}
}
func (p *processor) visit(v cue.Value) bool {
if e, ok := v.Elem(); ok {
p.updateValue(e)
p.visit(e)
}
if v.Kind() != cue.StructKind {
p.updateValue(v)
return true
}
p.stack = append(p.stack, v)
defer func() { p.stack = p.stack[:len(p.stack)-1] }()
for it, _ := v.Fields(cue.All()); it.Next(); {
p.updateValue(it.Value())
p.visit(it.Value())
}
for _, kv := range v.BulkOptionals() {
p.updateValue(kv[0])
p.visit(kv[0])
p.updateValue(kv[1])
p.visit(kv[1])
}
return false
}
func (p *processor) updateValue(v cue.Value) cue.Value {
switch op, a := v.Expr(); op {
case cue.NoOp:
return v
case cue.SelectorOp:
v := p.updateValue(a[0])
switch x := a[1].Source().(type) {
case *ast.SelectorExpr:
return p.lookup(v, x.Sel)
case *ast.Ident:
v := p.updateValue(a[0])
return p.lookup(v, x)
}
default:
for _, v := range a {
p.updateValue(v)
}
}
return v
}
func (p *processor) lookup(v cue.Value, l ast.Expr) cue.Value {
label, ok := l.(ast.Label)
if !ok {
return cue.Value{}
}
name, isIdent, err := ast.LabelName(label)
if err != nil {
return cue.Value{}
}
f, err := v.FieldByName(name, isIdent)
if err != nil {
f := v.Template()
if f == nil {
return cue.Value{}
}
return v.Template()(name)
}
switch {
case !p.done[l]:
p.done[l] = true
if !f.IsDefinition {
break
}
if !ast.IsValidIdent(name) {
p.err = errors.Append(p.err, errors.Newf(
l.Pos(),
"cannot convert reference to definition with invalid identifier %q",
name))
break
}
if ident, ok := l.(*ast.Ident); ok && !internal.IsDef(name) {
p.rename[ident] = "#" + name
}
}
return f.Value
}
// tagAmbiguous marks identifier fields were not handled by the previous pass.
// These can be identifiers within unused templates, for instance. It is
// possible to do further resolution within templates, but for now we will
// punt on this.
func (p *processor) tagAmbiguous(f *ast.File) {
ast.Walk(f, p.tagRef, nil)
}
func (p *processor) tagRef(n ast.Node) bool {
switch x := n.(type) {
case *ast.Field:
ast.Walk(x.Value, p.tagRef, nil)
lab := x.Label
if a, ok := x.Label.(*ast.Alias); ok {
lab, _ = a.Expr.(ast.Label)
}
switch lab.(type) {
case *ast.Ident, *ast.BasicLit:
default: // list, paren, or interpolation
ast.Walk(lab, p.tagRef, nil)
}
return false
case *ast.Ident:
if _, ok := p.done[x]; !ok {
p.ambiguous[x.Name] = append(p.ambiguous[x.Name], x.Pos())
}
return false
}
return true
}
func (p *processor) rewriteIdents() {
for x, name := range p.rename {
x.Name = name
}
}
func (p *processor) renameFields(f *ast.File) {
hasErr := false
_ = astutil.Apply(f, func(c astutil.Cursor) bool {
switch x := c.Node().(type) {
case *ast.Field:
if x.Token != token.ISA {
return true
}
label, isIdent, err := ast.LabelName(x.Label)
if err != nil {
b, _ := format.Node(x.Label)
hasErr = true
p.err = errors.Append(p.err, errors.Newf(x.Pos(),
`cannot convert dynamic definition for '%s'`, string(b)))
return false
}
if !isIdent && !ast.IsValidIdent(label) {
hasErr = true
p.err = errors.Append(p.err, errors.Newf(x.Pos(),
`invalid identifier %q; definition must be valid label`, label))
return false
}
if refs, ok := p.ambiguous[label]; ok {
h := fnv.New32()
_, _ = h.Write([]byte(label))
opt := fmt.Sprintf("@tmpNoExportNewDef(%x)", h.Sum32()&0xffff)
f := &ast.Field{
Label: ast.NewIdent(label),
Value: ast.NewIdent("#" + label),
Attrs: []*ast.Attribute{{Text: opt}},
}
c.InsertAfter(f)
b := &strings.Builder{}
fmt.Fprintln(b, "Possible references to this location:")
for _, p := range refs {
fmt.Fprintf(b, "\t%s\n", p)
}
cg := internal.NewComment(true, b.String())
astutil.CopyPosition(cg, c.Node())
ast.AddComment(c.Node(), cg)
}
x.Label = ast.NewIdent("#" + label)
x.Token = token.COLON
}
return true
}, nil)
if hasErr {
p.err = errors.Append(p.err, errors.Newf(token.NoPos, `Incompatible definitions detected:
A trick that can be used is to rename this to a regular identifier and then
move the definition to a sub field. For instance, rewrite
"foo-bar" :: baz
"foo\(bar)" :: baz
to
#defmap: "foo-bar": baz
#defmap: "foo\(bar)": baz
Errors:`))
}
}
func (p *processor) visitAll(fn func(f *ast.File)) {
if p.err != nil {
return
}
done := map[*ast.File]bool{}
for _, b := range p.instances {
for _, f := range b.Files {
if done[f] {
continue
}
done[f] = true
fn(f)
}
}
}