github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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  	"github.com/joomcode/cue/cue/ast"
    23  	"github.com/joomcode/cue/cue/errors"
    24  	"github.com/joomcode/cue/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  //
    42  func Sanitize(f *ast.File) error {
    43  	z := &sanitizer{
    44  		file: f,
    45  		rand: rand.New(rand.NewSource(808)),
    46  
    47  		names:      map[string]bool{},
    48  		importMap:  map[string]*ast.ImportSpec{},
    49  		referenced: map[ast.Node]bool{},
    50  		altMap:     map[ast.Node]string{},
    51  	}
    52  
    53  	// Gather all names.
    54  	walk(&scope{
    55  		errFn:   z.errf,
    56  		nameFn:  z.addName,
    57  		identFn: z.markUsed,
    58  	}, f)
    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  	walk(s, f)
    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  	z.file.VisitImports(func(d *ast.ImportDecl) {
   173  		k := 0
   174  		for _, s := range d.Specs {
   175  			if _, ok := z.referenced[s]; ok {
   176  				d.Specs[k] = s
   177  				k++
   178  			}
   179  		}
   180  		d.Specs = d.Specs[:k]
   181  	})
   182  }
   183  
   184  func (z *sanitizer) handleIdent(s *scope, n *ast.Ident) bool {
   185  	if n.Node == nil {
   186  		return true
   187  	}
   188  
   189  	_, _, node := s.lookup(n.Name)
   190  	if node.node == nil {
   191  		spec, ok := n.Node.(*ast.ImportSpec)
   192  		if !ok {
   193  			// Clear node. A reference may have been moved to a different
   194  			// file. If not, it should be an error.
   195  			n.Node = nil
   196  			n.Scope = nil
   197  			return false
   198  		}
   199  
   200  		_ = z.addImport(spec)
   201  		info, _ := ParseImportSpec(spec)
   202  		z.fileScope.insert(info.Ident, spec, spec)
   203  		return true
   204  	}
   205  
   206  	if x, ok := n.Node.(*ast.ImportSpec); ok {
   207  		xi, _ := ParseImportSpec(x)
   208  
   209  		if y, ok := node.node.(*ast.ImportSpec); ok {
   210  			yi, _ := ParseImportSpec(y)
   211  			if xi.ID == yi.ID { // name must be identical as a result of lookup.
   212  				z.referenced[y] = true
   213  				n.Node = x
   214  				n.Scope = nil
   215  				return false
   216  			}
   217  		}
   218  
   219  		// Either:
   220  		// - the import is shadowed
   221  		// - an incorrect import is matched
   222  		// In all cases we need to create a new import with a unique name or
   223  		// use a previously created one.
   224  		spec := z.importMap[xi.ID]
   225  		if spec == nil {
   226  			name := z.uniqueName(xi.Ident, false)
   227  			spec = z.addImport(&ast.ImportSpec{
   228  				Name: ast.NewIdent(name),
   229  				Path: x.Path,
   230  			})
   231  			z.importMap[xi.ID] = spec
   232  			z.fileScope.insert(name, spec, spec)
   233  		}
   234  
   235  		info, _ := ParseImportSpec(spec)
   236  		// TODO(apply): replace n itself directly
   237  		n.Name = info.Ident
   238  		n.Node = spec
   239  		n.Scope = nil
   240  		return false
   241  	}
   242  
   243  	if node.node == n.Node {
   244  		return true
   245  	}
   246  
   247  	// n.Node != node and are both not nil and n.Node is not an ImportSpec.
   248  	// This means that either n.Node is illegal or shadowed.
   249  	// Look for the scope in which n.Node is defined and add an alias or let.
   250  
   251  	parent, e, ok := s.resolveScope(n.Name, n.Node)
   252  	if !ok {
   253  		// The node isn't within a legal scope within this file. It may only
   254  		// possibly shadow a value of another file. We add a top-level let
   255  		// clause to refer to this value.
   256  
   257  		// TODO(apply): better would be to have resolve use Apply so that we can replace
   258  		// the entire ast.Ident, rather than modifying it.
   259  		// TODO: resolve to new node or rely on another pass of Resolve?
   260  		n.Name = z.unshadow(z.file, n.Name, n)
   261  		n.Node = nil
   262  		n.Scope = nil
   263  
   264  		return false
   265  	}
   266  
   267  	var name string
   268  	// var isNew bool
   269  	switch x := e.link.(type) {
   270  	case *ast.Field: // referring to regular field.
   271  		name, ok = z.altMap[x]
   272  		if ok {
   273  			break
   274  		}
   275  		// If this field has not alias, introduce one with a unique name.
   276  		// If this has an alias, also introduce a new name. There is a
   277  		// possibility that the alias can be used, but it is easier to just
   278  		// assign a new name, assuming this case is rather rare.
   279  		switch y := x.Label.(type) {
   280  		case *ast.Alias:
   281  			name = z.unshadow(parent, y.Ident.Name, y)
   282  
   283  		case *ast.Ident:
   284  			var isNew bool
   285  			name, isNew = z.addRename(y.Name, x)
   286  			if isNew {
   287  				ident := ast.NewIdent(name)
   288  				// Move formatting and comments from original label to alias
   289  				// identifier.
   290  				CopyMeta(ident, y)
   291  				ast.SetRelPos(y, token.NoRelPos)
   292  				ast.SetComments(y, nil)
   293  				x.Label = &ast.Alias{Ident: ident, Expr: y}
   294  			}
   295  
   296  		default:
   297  			// This is an illegal reference.
   298  			return false
   299  		}
   300  
   301  	case *ast.LetClause:
   302  		name = z.unshadow(parent, x.Ident.Name, x)
   303  
   304  	case *ast.Alias:
   305  		name = z.unshadow(parent, x.Ident.Name, x)
   306  
   307  	default:
   308  		panic(fmt.Sprintf("unexpected link type %T", e.link))
   309  	}
   310  
   311  	// TODO(apply): better would be to have resolve use Apply so that we can replace
   312  	// the entire ast.Ident, rather than modifying it.
   313  	n.Name = name
   314  	n.Node = nil
   315  	n.Scope = nil
   316  
   317  	return true
   318  }
   319  
   320  // uniqueName returns a new name globally unique name of the form
   321  // base_XX ... base_XXXXXXXXXXXXXX or _base or the same pattern with a '_'
   322  // prefix if hidden is true.
   323  //
   324  // It prefers short extensions over large ones, while ensuring the likelihood of
   325  // fast termination is high. There are at least two digits to make it visually
   326  // clearer this concerns a generated number.
   327  //
   328  func (z *sanitizer) uniqueName(base string, hidden bool) string {
   329  	if hidden && !strings.HasPrefix(base, "_") {
   330  		base = "_" + base
   331  		if !z.names[base] {
   332  			z.names[base] = true
   333  			return base
   334  		}
   335  	}
   336  
   337  	// TODO(go1.13): const mask = 0xff_ffff_ffff_ffff
   338  	const mask = 0xffffffffffffff // 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  }