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 }