cuelang.org/go@v0.13.0/cue/ast/astutil/sanitize.go (about)

     1  // Copyright 2020 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  
    15  package astutil
    16  
    17  import (
    18  	"fmt"
    19  	"math/rand"
    20  	"strings"
    21  
    22  	"cuelang.org/go/cue/ast"
    23  	"cuelang.org/go/cue/errors"
    24  	"cuelang.org/go/cue/token"
    25  )
    26  
    27  // TODO:
    28  // - handle comprehensions
    29  // - change field from foo to "foo" if it isn't referenced, rather than
    30  //   relying on introducing a unique alias.
    31  // - change a predeclared identifier reference to use the __ident form,
    32  //   instead of introducing an alias.
    33  
    34  // Sanitize rewrites File f in place to be well-formed after automated
    35  // construction of an AST.
    36  //
    37  // Rewrites:
    38  //   - auto inserts imports associated with Idents
    39  //   - unshadows imports associated with idents
    40  //   - unshadows references for identifiers that were already resolved.
    41  func Sanitize(f *ast.File) error {
    42  	z := &sanitizer{
    43  		file: f,
    44  		rand: rand.New(rand.NewSource(808)),
    45  
    46  		names:      map[string]bool{},
    47  		importMap:  map[string]*ast.ImportSpec{},
    48  		referenced: map[ast.Node]bool{},
    49  		altMap:     map[ast.Node]string{},
    50  	}
    51  
    52  	// Gather all names.
    53  	s := &scope{
    54  		errFn:   z.errf,
    55  		nameFn:  z.addName,
    56  		identFn: z.markUsed,
    57  	}
    58  	ast.Walk(f, s.Before, nil)
    59  	if z.errs != nil {
    60  		return z.errs
    61  	}
    62  
    63  	// Add imports and unshadow.
    64  	s = &scope{
    65  		file:    f,
    66  		errFn:   z.errf,
    67  		identFn: z.handleIdent,
    68  		index:   make(map[string]entry),
    69  	}
    70  	z.fileScope = s
    71  	ast.Walk(f, s.Before, nil)
    72  	if z.errs != nil {
    73  		return z.errs
    74  	}
    75  
    76  	z.cleanImports()
    77  
    78  	return z.errs
    79  }
    80  
    81  type sanitizer struct {
    82  	file      *ast.File
    83  	fileScope *scope
    84  
    85  	rand *rand.Rand
    86  
    87  	// names is all used names. Can be used to determine a new unique name.
    88  	names      map[string]bool
    89  	referenced map[ast.Node]bool
    90  
    91  	// altMap defines an alternative name for an existing entry link (a field,
    92  	// alias or let clause). As new names are globally unique, they can be
    93  	// safely reused for any unshadowing.
    94  	altMap    map[ast.Node]string
    95  	importMap map[string]*ast.ImportSpec
    96  
    97  	errs errors.Error
    98  }
    99  
   100  func (z *sanitizer) errf(p token.Pos, msg string, args ...interface{}) {
   101  	z.errs = errors.Append(z.errs, errors.Newf(p, msg, args...))
   102  }
   103  
   104  func (z *sanitizer) addName(name string) {
   105  	z.names[name] = true
   106  }
   107  
   108  func (z *sanitizer) addRename(base string, n ast.Node) (alt string, new bool) {
   109  	if name, ok := z.altMap[n]; ok {
   110  		return name, false
   111  	}
   112  
   113  	name := z.uniqueName(base, false)
   114  	z.altMap[n] = name
   115  	return name, true
   116  }
   117  
   118  func (z *sanitizer) unshadow(parent ast.Node, base string, link ast.Node) string {
   119  	name, ok := z.altMap[link]
   120  	if !ok {
   121  		name = z.uniqueName(base, false)
   122  		z.altMap[link] = name
   123  
   124  		// Insert new let clause at top to refer to a declaration in possible
   125  		// other files.
   126  		let := &ast.LetClause{
   127  			Ident: ast.NewIdent(name),
   128  			Expr:  ast.NewIdent(base),
   129  		}
   130  
   131  		var decls *[]ast.Decl
   132  
   133  		switch x := parent.(type) {
   134  		case *ast.File:
   135  			decls = &x.Decls
   136  		case *ast.StructLit:
   137  			decls = &x.Elts
   138  		default:
   139  			panic(fmt.Sprintf("impossible scope type %T", parent))
   140  		}
   141  
   142  		i := 0
   143  		for ; i < len(*decls); i++ {
   144  			if (*decls)[i] == link {
   145  				break
   146  			}
   147  			if f, ok := (*decls)[i].(*ast.Field); ok && f.Label == link {
   148  				break
   149  			}
   150  		}
   151  
   152  		if i > 0 {
   153  			ast.SetRelPos(let, token.NewSection)
   154  		}
   155  
   156  		a := append((*decls)[:i:i], let)
   157  		*decls = append(a, (*decls)[i:]...)
   158  	}
   159  	return name
   160  }
   161  
   162  func (z *sanitizer) markUsed(s *scope, n *ast.Ident) bool {
   163  	if n.Node != nil {
   164  		return false
   165  	}
   166  	_, _, entry := s.lookup(n.String())
   167  	z.referenced[entry.link] = true
   168  	return true
   169  }
   170  
   171  func (z *sanitizer) cleanImports() {
   172  	var fileImports []*ast.ImportSpec
   173  	z.file.VisitImports(func(decl *ast.ImportDecl) {
   174  		newLen := 0
   175  		for _, spec := range decl.Specs {
   176  			if _, ok := z.referenced[spec]; ok {
   177  				fileImports = append(fileImports, spec)
   178  				decl.Specs[newLen] = spec
   179  				newLen++
   180  			}
   181  		}
   182  		decl.Specs = decl.Specs[:newLen]
   183  	})
   184  	z.file.Imports = fileImports
   185  	// Ensure that the first import always starts a new section
   186  	// so that if the file has a comment, it won't be associated with
   187  	// the import comment rather than the file.
   188  	first := true
   189  	z.file.VisitImports(func(decl *ast.ImportDecl) {
   190  		if first {
   191  			ast.SetRelPos(decl, token.NewSection)
   192  			first = false
   193  		}
   194  	})
   195  }
   196  
   197  func (z *sanitizer) handleIdent(s *scope, n *ast.Ident) bool {
   198  	if n.Node == nil {
   199  		return true
   200  	}
   201  
   202  	_, _, node := s.lookup(n.Name)
   203  	if node.node == nil {
   204  		spec, ok := n.Node.(*ast.ImportSpec)
   205  		if !ok {
   206  			// Clear node. A reference may have been moved to a different
   207  			// file. If not, it should be an error.
   208  			n.Node = nil
   209  			n.Scope = nil
   210  			return false
   211  		}
   212  
   213  		_ = z.addImport(spec)
   214  		info, _ := ParseImportSpec(spec)
   215  		z.fileScope.insert(info.Ident, spec, spec)
   216  		return true
   217  	}
   218  
   219  	if x, ok := n.Node.(*ast.ImportSpec); ok {
   220  		xi, _ := ParseImportSpec(x)
   221  
   222  		if y, ok := node.node.(*ast.ImportSpec); ok {
   223  			yi, _ := ParseImportSpec(y)
   224  			if xi.ID == yi.ID { // name must be identical as a result of lookup.
   225  				z.referenced[y] = true
   226  				n.Node = x
   227  				n.Scope = nil
   228  				return false
   229  			}
   230  		}
   231  
   232  		// Either:
   233  		// - the import is shadowed
   234  		// - an incorrect import is matched
   235  		// In all cases we need to create a new import with a unique name or
   236  		// use a previously created one.
   237  		spec := z.importMap[xi.ID]
   238  		if spec == nil {
   239  			name := z.uniqueName(xi.Ident, false)
   240  			spec = z.addImport(&ast.ImportSpec{
   241  				Name: ast.NewIdent(name),
   242  				Path: x.Path,
   243  			})
   244  			z.importMap[xi.ID] = spec
   245  			z.fileScope.insert(name, spec, spec)
   246  		}
   247  
   248  		info, _ := ParseImportSpec(spec)
   249  		// TODO(apply): replace n itself directly
   250  		n.Name = info.Ident
   251  		n.Node = spec
   252  		n.Scope = nil
   253  		return false
   254  	}
   255  
   256  	if node.node == n.Node {
   257  		return true
   258  	}
   259  
   260  	// n.Node != node and are both not nil and n.Node is not an ImportSpec.
   261  	// This means that either n.Node is illegal or shadowed.
   262  	// Look for the scope in which n.Node is defined and add an alias or let.
   263  
   264  	parent, e, ok := s.resolveScope(n.Name, n.Node)
   265  	if !ok {
   266  		// The node isn't within a legal scope within this file. It may only
   267  		// possibly shadow a value of another file. We add a top-level let
   268  		// clause to refer to this value.
   269  
   270  		// TODO(apply): better would be to have resolve use Apply so that we can replace
   271  		// the entire ast.Ident, rather than modifying it.
   272  		// TODO: resolve to new node or rely on another pass of Resolve?
   273  		n.Name = z.unshadow(z.file, n.Name, n)
   274  		n.Node = nil
   275  		n.Scope = nil
   276  
   277  		return false
   278  	}
   279  
   280  	var name string
   281  	// var isNew bool
   282  	switch x := e.link.(type) {
   283  	case *ast.Field: // referring to regular field.
   284  		name, ok = z.altMap[x]
   285  		if ok {
   286  			break
   287  		}
   288  		// If this field has not alias, introduce one with a unique name.
   289  		// If this has an alias, also introduce a new name. There is a
   290  		// possibility that the alias can be used, but it is easier to just
   291  		// assign a new name, assuming this case is rather rare.
   292  		switch y := x.Label.(type) {
   293  		case *ast.Alias:
   294  			name = z.unshadow(parent, y.Ident.Name, y)
   295  
   296  		case *ast.Ident:
   297  			var isNew bool
   298  			name, isNew = z.addRename(y.Name, x)
   299  			if isNew {
   300  				ident := ast.NewIdent(name)
   301  				// Move formatting and comments from original label to alias
   302  				// identifier.
   303  				CopyMeta(ident, y)
   304  				ast.SetRelPos(y, token.NoRelPos)
   305  				ast.SetComments(y, nil)
   306  				x.Label = &ast.Alias{Ident: ident, Expr: y}
   307  			}
   308  
   309  		default:
   310  			// This is an illegal reference.
   311  			return false
   312  		}
   313  
   314  	case *ast.LetClause:
   315  		name = z.unshadow(parent, x.Ident.Name, x)
   316  
   317  	case *ast.Alias:
   318  		name = z.unshadow(parent, x.Ident.Name, x)
   319  
   320  	default:
   321  		panic(fmt.Sprintf("unexpected link type %T", e.link))
   322  	}
   323  
   324  	// TODO(apply): better would be to have resolve use Apply so that we can replace
   325  	// the entire ast.Ident, rather than modifying it.
   326  	n.Name = name
   327  	n.Node = nil
   328  	n.Scope = nil
   329  
   330  	return true
   331  }
   332  
   333  // uniqueName returns a new name globally unique name of the form
   334  // base_NN ... base_NNNNNNNNNNNNNN or _base or the same pattern with a '_'
   335  // prefix if hidden is true.
   336  //
   337  // It prefers short extensions over large ones, while ensuring the likelihood of
   338  // fast termination is high. There are at least two digits to make it visually
   339  // clearer this concerns a generated number.
   340  func (z *sanitizer) uniqueName(base string, hidden bool) string {
   341  	if hidden && !strings.HasPrefix(base, "_") {
   342  		base = "_" + base
   343  		if !z.names[base] {
   344  			z.names[base] = true
   345  			return base
   346  		}
   347  	}
   348  
   349  	const mask = 0xff_ffff_ffff_ffff // max bits; stay clear of int64 overflow
   350  	const shift = 4                  // rate of growth
   351  	for n := int64(0x10); ; n = mask&((n<<shift)-1) + 1 {
   352  		num := z.rand.Intn(int(n))
   353  		name := fmt.Sprintf("%s_%01X", base, num)
   354  		if !z.names[name] {
   355  			z.names[name] = true
   356  			return name
   357  		}
   358  	}
   359  }
   360  
   361  func (z *sanitizer) addImport(spec *ast.ImportSpec) *ast.ImportSpec {
   362  	spec = insertImport(&z.file.Decls, spec)
   363  	z.referenced[spec] = true
   364  	return spec
   365  }