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 }