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