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 }