this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

tools/fix: extract and extend explicitopen fix

Extract the explicitopen fix logic into fixopen.go
and extend it with several improvements:

- Hoist embedded close() to wrapper level: under old
semantics, embedding close() opened up the embedding.
Under explicitopen it no longer does, so the fix now
removes close() from the embedding and applies it as
a wrapper around the containing struct.
- Wrapper selection: close-only uses close(), close
with other refs uses close(__reclose()), close with
defs uses __closeAll (which subsumes close).
- Single-embed simplification: structs with a single
embedding are simplified to the expression directly
without wrapping.
- Add TODO comment for top-level definition embeddings,
which are likely intended as schema constraints.
- Split tests into explicitopen.txtar (basic cases),
explicitopen_expr.txtar (close/conjunction/disjunction
expressions), and explicitopen_shorthands.txtar
(single-embed shortcuts and comprehension TODOs).

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: If2d6b35c07dffb24b7ada346653ff2929b07fc08
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1236310
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>

+914 -181
+1 -1
cmd/cue/cmd/testdata/script/fix_explicitopen.txtar
··· 27 27 #A: a: int 28 28 #B: b: int 29 29 30 - X: __reclose({ 30 + X: __closeAll({ 31 31 #A... // foo 32 32 33 33 // bar
+1 -1
cmd/cue/cmd/testdata/script/modupgrade.txtar
··· 34 34 #A: a: int 35 35 #B: b: int 36 36 37 - X: __reclose({ 37 + X: __closeAll({ 38 38 #A... // foo 39 39 40 40 // bar
-148
tools/fix/fix.go
··· 27 27 "cuelang.org/go/cue/ast/astutil" 28 28 "cuelang.org/go/cue/errors" 29 29 "cuelang.org/go/cue/token" 30 - "cuelang.org/go/internal" 31 30 "cuelang.org/go/internal/cueexperiment" 32 31 "cuelang.org/go/internal/mod/semver" 33 32 ) ··· 290 289 } 291 290 292 291 return result 293 - } 294 - 295 - type closeInfo struct { 296 - // Do not close enclosing structs if non-zero. This may be the case 297 - // for comprehensions, nested structs, etc. 298 - suspendReclose int 299 - 300 - // If the current scope embedded a definition, we know for sure the 301 - // enclosing struct needs to be closed. 302 - embedsDefinition bool 303 - } 304 - 305 - func (c closeInfo) shouldReclose() bool { 306 - return c.suspendReclose == 0 307 - } 308 - 309 - func fixExplicitOpen(f *ast.File) (result *ast.File, hasChanges bool) { 310 - 311 - var info closeInfo 312 - recloseStack := []closeInfo{} 313 - result = astutil.Apply(f, func(c astutil.Cursor) bool { 314 - n := c.Node() 315 - switch n := n.(type) { 316 - case *ast.Field: 317 - recloseStack = append(recloseStack, info) 318 - info = closeInfo{} 319 - if internal.IsDefinition(n.Label) { 320 - info.suspendReclose++ 321 - } 322 - 323 - case *ast.BinaryExpr: 324 - if n.Op == token.AND || n.Op == token.OR { 325 - recloseStack = append(recloseStack, info) 326 - info = closeInfo{} 327 - } 328 - 329 - case *ast.Comprehension: 330 - info.suspendReclose++ 331 - 332 - case *ast.EmbedDecl: 333 - // Disable closing embedded structs. 334 - info.suspendReclose++ 335 - 336 - // Check if the embedded expression needs to be "opened" with ellipsis 337 - switch x := n.Expr.(type) { 338 - case *ast.PostfixExpr: 339 - // Already has ellipsis 340 - return true 341 - case *ast.BinaryExpr: 342 - // Needs to be handled per term. 343 - if x.Op != token.AND && x.Op != token.OR { 344 - return true 345 - } 346 - case *ast.Ident: 347 - if x.Name == "_" { 348 - return true 349 - } 350 - if internal.IsDefinition(x) { 351 - info.embedsDefinition = true 352 - } 353 - case *ast.CallExpr: 354 - if fun, ok := x.Fun.(*ast.SelectorExpr); ok { 355 - id, ok := fun.X.(*ast.Ident) 356 - if !ok { 357 - break 358 - } 359 - sel, ok := fun.Sel.(*ast.Ident) 360 - if !ok { 361 - break 362 - } 363 - i, ok := id.Node.(*ast.ImportSpec) 364 - if !ok { 365 - break 366 - } 367 - switch i.Path.Value { 368 - case `"list"`: 369 - switch sel.Name { 370 - case "Avg", "Sum", "Max", "Min", "Product", 371 - "MaxItems", "MinItems", 372 - "Contains", "UniqueItems", 373 - "Range", 374 - "SortStrings", 375 - "IsSorted", "IsSortedStrings": 376 - return true 377 - } 378 - default: 379 - return true 380 - } 381 - } 382 - case *ast.ListLit, // Lists cannot be opened anyway (atm). 383 - *ast.StructLit, // Structs are open by default 384 - *ast.BasicLit, 385 - *ast.Interpolation, 386 - *ast.UnaryExpr: 387 - 388 - return true 389 - default: 390 - // Needs ellipsis 391 - } 392 - 393 - // Transform the embedding to use postfix ellipsis 394 - postfixExpr := &ast.PostfixExpr{ 395 - X: n.Expr, 396 - Op: token.ELLIPSIS, 397 - OpPos: n.Expr.End(), 398 - } 399 - 400 - // After is not called after a Replace: match nesting count. 401 - info.suspendReclose-- 402 - c.Replace(&ast.EmbedDecl{ 403 - Expr: postfixExpr, 404 - }) 405 - hasChanges = true 406 - } 407 - return true 408 - }, func(c astutil.Cursor) bool { 409 - switch n := c.Node().(type) { 410 - case *ast.Field: 411 - info = recloseStack[len(recloseStack)-1] 412 - recloseStack = recloseStack[:len(recloseStack)-1] 413 - c.ClearEnclosingModified() 414 - 415 - case *ast.BinaryExpr: 416 - if n.Op == token.AND || n.Op == token.OR { 417 - info = recloseStack[len(recloseStack)-1] 418 - recloseStack = recloseStack[:len(recloseStack)-1] 419 - c.ClearEnclosingModified() 420 - } 421 - 422 - case *ast.Comprehension, *ast.EmbedDecl: 423 - info.suspendReclose-- 424 - 425 - case *ast.StructLit: 426 - if c.Modified() && info.shouldReclose() { 427 - ast.SetRelPos(n, token.NoSpace) 428 - // TODO: we could use embedsDefinition to select whether to 429 - // use __reclose or __closeAll. For now, always use __reclose, 430 - // as we need to verify some of the edge cases around this. 431 - hasChanges = true 432 - c.Replace(ast.NewCall(ast.NewIdent("__reclose"), n)) 433 - c.ClearEnclosingModified() 434 - } 435 - } 436 - return true 437 - }).(*ast.File) 438 - 439 - return result, hasChanges 440 292 } 441 293 442 294 func fixAliasV2(f *ast.File) (result *ast.File, hasChanges bool) {
+1 -1
tools/fix/fix_test.go
··· 120 120 #A: a: int 121 121 #B: b: int 122 122 123 - X: __reclose({ 123 + X: __closeAll({ 124 124 #A... // foo 125 125 126 126 // bar
+369
tools/fix/fixopen.go
··· 1 + // Copyright 2025 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 fix 16 + 17 + import ( 18 + "cuelang.org/go/cue/ast" 19 + "cuelang.org/go/cue/ast/astutil" 20 + "cuelang.org/go/cue/token" 21 + "cuelang.org/go/internal" 22 + ) 23 + 24 + func todoComment(msg string) *ast.CommentGroup { 25 + return &ast.CommentGroup{ 26 + Doc: true, 27 + List: []*ast.Comment{{Text: "// TODO(cue-fix): " + msg}}, 28 + } 29 + } 30 + 31 + // embedFlags tracks what kind of closing an embedding requires. 32 + type embedFlags struct { 33 + def bool // a definition was embedded 34 + other bool // another embedding was modified (needs runtime check) 35 + forceReclose bool // disjunction has non-def operand; __closeAll would be wrong 36 + close bool // close() was embedded and hoisted to wrapper level 37 + } 38 + 39 + func (a embedFlags) or(b embedFlags) embedFlags { 40 + return embedFlags{ 41 + def: a.def || b.def, 42 + other: a.other || b.other, 43 + forceReclose: a.forceReclose || b.forceReclose, 44 + close: a.close || b.close, 45 + } 46 + } 47 + 48 + type closeInfo struct { 49 + // Do not close enclosing structs if non-zero. This may be the case 50 + // for comprehensions, nested structs, etc. 51 + suspendReclose int 52 + 53 + // inComprehension tracks whether we are inside a comprehension value. 54 + inComprehension int 55 + 56 + embedFlags 57 + } 58 + 59 + func (c closeInfo) shouldReclose() bool { 60 + return c.suspendReclose == 0 61 + } 62 + 63 + func fixExplicitOpen(f *ast.File) (result *ast.File, hasChanges bool) { 64 + 65 + var info closeInfo 66 + recloseStack := []closeInfo{} 67 + result = astutil.Apply(f, func(c astutil.Cursor) bool { 68 + n := c.Node() 69 + switch n := n.(type) { 70 + case *ast.Field: 71 + recloseStack = append(recloseStack, info) 72 + info = closeInfo{} 73 + if internal.IsDefinition(n.Label) { 74 + info.suspendReclose++ 75 + } 76 + 77 + case *ast.BinaryExpr: 78 + if n.Op == token.AND || n.Op == token.OR { 79 + recloseStack = append(recloseStack, info) 80 + info = closeInfo{} 81 + } 82 + 83 + case *ast.Comprehension: 84 + info.suspendReclose++ 85 + info.inComprehension++ 86 + 87 + case *ast.EmbedDecl: 88 + info.suspendReclose++ 89 + 90 + newExpr, exprChanged, flags := openEmbedExpr(n.Expr) 91 + info.embedFlags = info.embedFlags.or(flags) 92 + if exprChanged { 93 + if info.inComprehension > 0 { 94 + ast.AddComment(newExpr, todoComment( 95 + "... may not be intended inside a comprehension value; consider removing it.")) 96 + } 97 + if flags.def && len(recloseStack) == 0 { 98 + ast.AddComment(newExpr, todoComment( 99 + "top-level definition embedding opened; if this is intended as a schema, remove the '...'.")) 100 + } 101 + newEmbed := &ast.EmbedDecl{Expr: newExpr} 102 + // After is not called after a Replace: match nesting count. 103 + info.suspendReclose-- 104 + c.Replace(newEmbed) 105 + hasChanges = true 106 + } 107 + } 108 + return true 109 + }, func(c astutil.Cursor) bool { 110 + switch n := c.Node().(type) { 111 + case *ast.Field: 112 + info = recloseStack[len(recloseStack)-1] 113 + recloseStack = recloseStack[:len(recloseStack)-1] 114 + c.ClearEnclosingModified() 115 + 116 + case *ast.BinaryExpr: 117 + if n.Op == token.AND || n.Op == token.OR { 118 + info = recloseStack[len(recloseStack)-1] 119 + recloseStack = recloseStack[:len(recloseStack)-1] 120 + c.ClearEnclosingModified() 121 + } 122 + 123 + case *ast.Comprehension: 124 + info.suspendReclose-- 125 + info.inComprehension-- 126 + 127 + case *ast.EmbedDecl: 128 + info.suspendReclose-- 129 + 130 + case *ast.StructLit: 131 + if c.Modified() && info.shouldReclose() { 132 + hasChanges = true 133 + 134 + if isSingleEmbed(n) { 135 + // Single embedding: { expr } ≡ expr, so use the 136 + // expression directly without wrapping. Strip the 137 + // ... since it is not needed outside a wrapper. 138 + // 139 + // This is done in post-processing (after ... was 140 + // added) because at the EmbedDecl pre-visit level 141 + // we don't yet know the parent struct's element 142 + // count. 143 + // 144 + // TODO: this incorrectly fires for single-embed 145 + // structs inside close() at field-value level, 146 + // e.g. a: close({_repo}). Fix by tracking whether 147 + // we are inside a close() argument. 148 + embed := n.Elts[0].(*ast.EmbedDecl) 149 + pf := embed.Expr.(*ast.PostfixExpr) 150 + embed.Expr = pf.X 151 + c.ClearEnclosingModified() 152 + break 153 + } 154 + 155 + // Single close()-hoisted embed (bare embedding, no ...): 156 + // under old semantics, embedding close() opened up, so 157 + // a single close() embed produces an open struct — no 158 + // wrapping needed. 159 + if isSingleBareEmbed(n) { 160 + c.ClearEnclosingModified() 161 + break 162 + } 163 + 164 + ast.SetRelPos(n, token.NoSpace) 165 + var wrapper ast.Expr = n 166 + switch { 167 + case info.def && !info.forceReclose: 168 + wrapper = ast.NewCall(ast.NewIdent("__closeAll"), n) 169 + case info.other || info.forceReclose: 170 + wrapper = ast.NewCall(ast.NewIdent("__reclose"), n) 171 + if info.close { 172 + wrapper = ast.NewCall(ast.NewIdent("close"), wrapper) 173 + } 174 + case info.close: 175 + wrapper = ast.NewCall(ast.NewIdent("close"), n) 176 + } 177 + c.Replace(wrapper) 178 + c.ClearEnclosingModified() 179 + } 180 + } 181 + return true 182 + }).(*ast.File) 183 + 184 + return result, hasChanges 185 + } 186 + 187 + // collectEmbedFlags recurses into an expression to collect embedding flags 188 + // without modifying the expression. Used for & and | where we add ... to the 189 + // whole expression rather than individual operands. 190 + func collectEmbedFlags(expr ast.Expr) (ast.Expr, bool, embedFlags) { 191 + switch x := expr.(type) { 192 + case *ast.BinaryExpr: 193 + if x.Op == token.AND || x.Op == token.OR { 194 + _, _, xf := collectEmbedFlags(x.X) 195 + _, _, yf := collectEmbedFlags(x.Y) 196 + f := xf.or(yf) 197 + if x.Op == token.OR && (xf.other || yf.other) { 198 + f.forceReclose = true 199 + } 200 + return expr, false, f 201 + } 202 + case *ast.ParenExpr: 203 + return collectEmbedFlags(x.X) 204 + case *ast.Ident: 205 + if x.Name == "_" { 206 + return expr, false, embedFlags{} 207 + } 208 + if internal.IsDefinition(x) { 209 + return expr, false, embedFlags{def: true} 210 + } 211 + return expr, false, embedFlags{other: true} 212 + case *ast.CallExpr: 213 + if id, ok := x.Fun.(*ast.Ident); ok { 214 + switch id.Name { 215 + case "close": 216 + _, _, f := openCloseArg(x.Args[0]) 217 + return expr, false, f.or(embedFlags{close: true}) 218 + } 219 + } 220 + } 221 + return expr, false, embedFlags{} 222 + } 223 + 224 + // openEmbedExpr adds postfix ellipsis to embedded expressions. For & and | 225 + // expressions, it adds ... to the whole expression. For other expressions, it 226 + // adds ellipsis if needed based on the expression type. 227 + func openEmbedExpr(expr ast.Expr) (result ast.Expr, changed bool, flags embedFlags) { 228 + switch x := expr.(type) { 229 + case *ast.PostfixExpr: 230 + // Already has ellipsis 231 + return expr, false, embedFlags{} 232 + 233 + case *ast.BinaryExpr: 234 + if x.Op == token.AND || x.Op == token.OR { 235 + // Collect flags from operands, then add ... to the 236 + // entire expression rather than each operand. 237 + _, _, xFlags := collectEmbedFlags(x.X) 238 + _, _, yFlags := collectEmbedFlags(x.Y) 239 + f := xFlags.or(yFlags) 240 + if x.Op == token.OR && (xFlags.other || yFlags.other) { 241 + f.forceReclose = true 242 + } 243 + return addEllipsis(expr), true, f 244 + } 245 + // Other binary ops (e.g. +, *) don't need ellipsis. 246 + return expr, false, embedFlags{} 247 + 248 + case *ast.ParenExpr: 249 + // Recurse through parens to collect flags, then add ... 250 + // to the whole parenthesized expression. 251 + _, _, f := collectEmbedFlags(x.X) 252 + return addEllipsis(expr), true, f 253 + 254 + case *ast.Ident: 255 + if x.Name == "_" { 256 + return expr, false, embedFlags{} 257 + } 258 + if internal.IsDefinition(x) { 259 + return addEllipsis(expr), true, embedFlags{def: true} 260 + } 261 + return addEllipsis(expr), true, embedFlags{other: true} 262 + 263 + case *ast.CallExpr: 264 + if id, ok := x.Fun.(*ast.Ident); ok { 265 + switch id.Name { 266 + case "close": 267 + // In the old semantics, embedding close() opened up 268 + // the embedding — the outer struct stayed open. Under 269 + // explicitopen, close() no longer opens up when embedded. 270 + // Hoist close() to wrapper level: return the processed 271 + // argument as the new embedding, and set the close flag 272 + // so the containing struct gets close() wrapping. 273 + if len(x.Args) == 1 { 274 + newArg, _, f := openCloseArg(x.Args[0]) 275 + f.close = true 276 + astutil.CopyMeta(newArg, x) 277 + return newArg, true, f 278 + } 279 + return expr, true, embedFlags{close: true} 280 + case "and", "or": 281 + return addEllipsis(expr), true, embedFlags{other: true} 282 + } 283 + } 284 + return expr, false, embedFlags{} 285 + 286 + case *ast.ListLit, // Lists cannot be opened anyway (atm). 287 + *ast.StructLit, // Structs are open by default 288 + *ast.BasicLit, 289 + *ast.Interpolation, 290 + *ast.UnaryExpr: 291 + 292 + return expr, false, embedFlags{} 293 + } 294 + 295 + // Default: needs ellipsis (SelectorExpr, IndexExpr, etc.) 296 + return addEllipsis(expr), true, embedFlags{other: true} 297 + } 298 + 299 + // openCloseArg processes the argument of an embedded close() call, 300 + // adding ... to any embeddings inside a struct literal. For non-struct 301 + // arguments, it returns the flags without modifying the expression 302 + // (adding ... to a bare identifier inside close() is not valid). 303 + func openCloseArg(expr ast.Expr) (ast.Expr, bool, embedFlags) { 304 + s, ok := expr.(*ast.StructLit) 305 + if !ok { 306 + // Non-struct argument: process like a regular embedding so that 307 + // e.g. close(#A) → #A... when hoisted. 308 + newExpr, _, f := openEmbedExpr(expr) 309 + return newExpr, f.def || f.other || f.close, f 310 + } 311 + var f embedFlags 312 + var changed bool 313 + newElts := make([]ast.Decl, len(s.Elts)) 314 + copy(newElts, s.Elts) 315 + for i, d := range newElts { 316 + embed, ok := d.(*ast.EmbedDecl) 317 + if !ok { 318 + continue 319 + } 320 + newExpr, exprChanged, ef := openEmbedExpr(embed.Expr) 321 + f = f.or(ef) 322 + if exprChanged { 323 + changed = true 324 + newElts[i] = &ast.EmbedDecl{Expr: newExpr} 325 + } 326 + } 327 + if !changed { 328 + return expr, false, f 329 + } 330 + newStruct := *s 331 + newStruct.Elts = newElts 332 + return &newStruct, true, f 333 + } 334 + 335 + // isSingleEmbed reports whether s contains exactly one declaration 336 + // which is an embedding with postfix ellipsis (...). 337 + func isSingleEmbed(s *ast.StructLit) bool { 338 + if len(s.Elts) != 1 { 339 + return false 340 + } 341 + embed, ok := s.Elts[0].(*ast.EmbedDecl) 342 + if !ok { 343 + return false 344 + } 345 + pf, ok := embed.Expr.(*ast.PostfixExpr) 346 + if !ok || pf.Op != token.ELLIPSIS { 347 + return false 348 + } 349 + return true 350 + } 351 + 352 + // isSingleBareEmbed reports whether s contains exactly one declaration 353 + // which is an embedding without postfix ellipsis. This occurs when a 354 + // close() call was hoisted and its struct argument became a bare embedding. 355 + func isSingleBareEmbed(s *ast.StructLit) bool { 356 + if len(s.Elts) != 1 { 357 + return false 358 + } 359 + _, ok := s.Elts[0].(*ast.EmbedDecl) 360 + return ok 361 + } 362 + 363 + func addEllipsis(expr ast.Expr) *ast.PostfixExpr { 364 + return &ast.PostfixExpr{ 365 + X: expr, 366 + Op: token.ELLIPSIS, 367 + OpPos: expr.End(), 368 + } 369 + }
+3 -3
tools/fix/testdata/all.txtar
··· 27 27 28 28 #A: a: int 29 29 30 - X: __reclose({ 31 - #A... 32 - }) 30 + X: { 31 + #A 32 + } 33 33 #Y: {#A...}
+20 -25
tools/fix/testdata/explicitopen.txtar
··· 27 27 } 28 28 29 29 _repo: {} 30 - inList: { 31 - steps: [ 32 - for v in _repo.checkoutCode {v}, 33 - for v in installGo {v}, 34 - { 35 - name: "foo" 36 - }, 37 - ] 38 - } 39 30 inClose: { 40 31 workflows: close({ 41 32 _repo ··· 46 37 {#foo} 47 38 } 48 39 } 40 + nestedStructWithEmbed: { 41 + a: { 42 + {#Def} // doesn't count as single. 43 + b: int 44 + } 45 + } 46 + 49 47 -- b.cue -- 50 48 // Already tagged, so do not do any changes. 51 49 @experiment(explicitopen) ··· 70 68 71 69 #A: a: int 72 70 73 - X: __reclose({ 74 - #A... 75 - }) 71 + X: { 72 + #A 73 + } 76 74 str: "bar" 77 75 Y: { 78 76 // Do not add ellipsis where unnecessary. ··· 86 84 } 87 85 88 86 _repo: {} 89 - inList: { 90 - steps: [ 91 - for v in _repo.checkoutCode {v...}, 92 - for v in installGo {v...}, 93 - { 94 - name: "foo" 95 - }, 96 - ] 97 - } 98 87 inClose: { 99 - workflows: close(__reclose({ 100 - _repo... 101 - })) 88 + workflows: close({ 89 + _repo 90 + }) 102 91 } 103 92 nestedStruct: { 104 - containsTrybotTrailer: __reclose({ 93 + containsTrybotTrailer: { 105 94 {#foo...} 95 + } 96 + } 97 + nestedStructWithEmbed: { 98 + a: __closeAll({ 99 + {#Def...} // doesn't count as single. 100 + b: int 106 101 }) 107 102 } 108 103 --- b.cue
+366
tools/fix/testdata/explicitopen_expr.txtar
··· 1 + 2 + #exp:explicitopen 3 + 4 + # Test __closeAll vs __reclose wrapper selection for various embedding patterns: 5 + # close() variants, conjunctions, disjunctions, and combinations with fields. 6 + 7 + #no-coverage 8 + 9 + -- cue.mod/module.cue -- 10 + module: "builtins.test" 11 + 12 + language: version: "v0.15.0" 13 + 14 + -- in.cue -- 15 + package foo 16 + 17 + #A: a: int 18 + #D: x: int 19 + _repo: {} 20 + 21 + // close()-oAnd indeed, the post-fix is incorrect. nly embedding → no change needed. 22 + closeOnly: { 23 + v: { 24 + close({a: int}) 25 + b: int 26 + } 27 + t1: v & {c: int} @test(err, code=eval, path=c, contains="field not allowed") 28 + t2: v & {b: 1} @test(eq, {b: 1, a: int}) 29 + } 30 + 31 + 32 + 33 + // close() + non-def reference → __reclose wraps struct, close() left as-is. 34 + closeAndRef: { 35 + v: { 36 + close({a: int}) 37 + _repo 38 + } 39 + 40 + t1: v & {c: int} @test(err, code=eval, path=c, contains="field not allowed") 41 + t2: v & {a: 1} @test(eq, {a: 1}) 42 + } 43 + 44 + 45 + 46 + // Mixed definition + close() → __closeAll wraps struct, close() left as-is. 47 + mixedDefClose: { 48 + #E: x: y: {} 49 + v: { 50 + #E 51 + close({b: c: {}}) 52 + } 53 + 54 + t1: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 55 + t2: v & {b: d: 1} @test(err, code=eval, contains="field not allowed") 56 + t3: v & {b: c: d: 1} @test(err, code=eval, contains="field not allowed") 57 + t4: v & {b: c: {}} @test(eq, {b: {c: {}}, x: {y: {}}}) 58 + t5: v & {x: y: {}} @test(eq, {x: {y: {}}, b: {c: {}}}) 59 + 60 + } 61 + 62 + 63 + // Conjunct → single embed, simplified. 64 + conjunct: { 65 + #A: a: int 66 + _repo: {} 67 + 68 + v: { 69 + #A & _repo 70 + } 71 + 72 + t1: v & {a: 1} @test(eq, {a: 1}) 73 + t2: v & {b: 1} @test(err, code=eval) 74 + } 75 + 76 + #C1: {a?: int, c: int} 77 + #C2: {b?: int, c: int} 78 + conjunctComplex: { 79 + v: { 80 + #C1 & #C2 81 + d: int 82 + } 83 + // TODO: 84 + // The new semantics is more permissive here: 85 + // t1: v & {a: 1} @test(err, code=eval) 86 + // t2: v & {b: 1} @test(err, code=eval) 87 + 88 + t3: v & {c: 1} @test(eq, { 89 + c: 1 90 + d: int 91 + a?: _|_ 92 + b?: _|_ 93 + }) 94 + t4: v & {d: 1} @test(eq, { 95 + d: 1 96 + a?: _|_ 97 + c: int 98 + b?: _|_ 99 + }) 100 + t5: v & {e: 1} @test(err, code=eval) 101 + } 102 + 103 + 104 + // Disjunct → single embed, simplified. 105 + disjunct: { 106 + #A | _repo 107 + } 108 + 109 + // Parenthesized conjunct. 110 + parenConj: { 111 + (#A & _repo) 112 + } 113 + 114 + // Embedded close() with def arg → __closeAll wraps, close() left as-is. 115 + embeddedCloseOnly: { 116 + close(#A) 117 + } 118 + 119 + // Embedded close() with def arg + extra field → __closeAll wraps. 120 + embeddedCloseField: { 121 + #A: a: int 122 + v: { 123 + close(#A) 124 + b: int 125 + } 126 + 127 + t1: v & {a: 1} @test(eq, {a: 1, b: int}) 128 + t2: v & {b: 1} @test(eq, {b: 1, a: int}) 129 + t3: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 130 + } 131 + 132 + // Conjunction with extra field → not simplified, __closeAll wraps. 133 + conjunctField: { 134 + #A & _repo 135 + b: int 136 + } 137 + 138 + // Disjunction with extra field → not simplified, __reclose wraps. 139 + disjunctField: { 140 + #A | _repo 141 + b: int 142 + } 143 + 144 + // Pure-definition disjunction → __closeAll (all operands are defs). 145 + disjunctDefs: { 146 + #A | #D 147 + } 148 + 149 + // and() with extra field → not simplified, __reclose wraps. 150 + andField: { 151 + #A: {a: int, b?: int} 152 + #D: {a: int, c?: int} 153 + v: { 154 + and([#A, #D]) 155 + b: int 156 + } 157 + 158 + t1: v & {a: 1} @test(eq, {a: 1, b: int, c?: _|_}) 159 + t2: v & {b: 1} @test(eq, {a: int, b: 1, c?: _|_}) 160 + 161 + // TODO: the new semantics is more permissive here: 162 + // t3: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 163 + } 164 + 165 + // or() with extra field → not simplified, __reclose wraps. 166 + orField: { 167 + or([#A, #D]) 168 + b: int 169 + } 170 + 171 + -- out/errors.txt -- 172 + [eval] closeOnly.t1.c: field not allowed: 173 + ./in.cue:13:11 174 + [eval] closeAndRef.t1.c: field not allowed: 175 + ./in.cue:26:11 176 + [eval] mixedDefClose.t1.c: field not allowed: 177 + ./in.cue:40:11 178 + [eval] mixedDefClose.t2.b.d: field not allowed: 179 + ./in.cue:41:14 180 + [eval] mixedDefClose.t3.b.c.d: field not allowed: 181 + ./in.cue:42:17 182 + [eval] conjunct.t2.b: field not allowed: 183 + ./in.cue:59:11 184 + [eval] conjunctComplex.v.a: field not allowed: 185 + ./in.cue:62:7 186 + [eval] conjunctComplex.v.b: field not allowed: 187 + ./in.cue:63:7 188 + [eval] conjunctComplex.t3.a: field not allowed: 189 + ./in.cue:62:7 190 + [eval] conjunctComplex.t3.b: field not allowed: 191 + ./in.cue:63:7 192 + [eval] conjunctComplex.t4.a: field not allowed: 193 + ./in.cue:62:7 194 + [eval] conjunctComplex.t4.b: field not allowed: 195 + ./in.cue:63:7 196 + [eval] conjunctComplex.t5.e: field not allowed: 197 + ./in.cue:86:11 198 + [eval] conjunctComplex.t5.a: field not allowed: 199 + ./in.cue:62:7 200 + [eval] conjunctComplex.t5.b: field not allowed: 201 + ./in.cue:63:7 202 + [eval] embeddedCloseField.t3.c: field not allowed: 203 + ./in.cue:115:11 204 + [eval] andField.v.c: field not allowed: 205 + ./in.cue:138:15 206 + [eval] andField.t1.c: field not allowed: 207 + ./in.cue:138:15 208 + [eval] andField.t2.c: field not allowed: 209 + ./in.cue:138:15 210 + -- out/fixmod -- 211 + --- cue.mod/module.cue 212 + module: "builtins.test" 213 + language: { 214 + version: "v0.15.0" 215 + } 216 + --- in.cue 217 + @experiment(explicitopen) 218 + 219 + package foo 220 + 221 + #A: a: int 222 + #D: x: int 223 + _repo: {} 224 + 225 + // close()-oAnd indeed, the post-fix is incorrect. nly embedding → no change needed. 226 + closeOnly: { 227 + v: close({ 228 + {a: int} 229 + b: int 230 + }) 231 + t1: v & {c: int} @test(err, code=eval, path=c, contains="field not allowed") 232 + t2: v & {b: 1} @test(eq, {b: 1, a: int}) 233 + } 234 + 235 + // close() + non-def reference → __reclose wraps struct, close() left as-is. 236 + closeAndRef: { 237 + v: close(__reclose({ 238 + {a: int} 239 + _repo... 240 + })) 241 + 242 + t1: v & {c: int} @test(err, code=eval, path=c, contains="field not allowed") 243 + t2: v & {a: 1} @test(eq, {a: 1}) 244 + } 245 + 246 + // Mixed definition + close() → __closeAll wraps struct, close() left as-is. 247 + mixedDefClose: { 248 + #E: x: y: {} 249 + v: __closeAll({ 250 + #E... 251 + {b: c: {}} 252 + }) 253 + 254 + t1: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 255 + t2: v & {b: d: 1} @test(err, code=eval, contains="field not allowed") 256 + t3: v & {b: c: d: 1} @test(err, code=eval, contains="field not allowed") 257 + t4: v & {b: c: {}} @test(eq, {b: {c: {}}, x: {y: {}}}) 258 + t5: v & {x: y: {}} @test(eq, {x: {y: {}}, b: {c: {}}}) 259 + } 260 + 261 + // Conjunct → single embed, simplified. 262 + conjunct: { 263 + #A: a: int 264 + _repo: {} 265 + 266 + v: { 267 + #A & _repo 268 + } 269 + 270 + t1: v & {a: 1} @test(eq, {a: 1}) 271 + t2: v & {b: 1} @test(err, code=eval) 272 + } 273 + 274 + #C1: {a?: int, c: int} 275 + #C2: {b?: int, c: int} 276 + conjunctComplex: { 277 + v: __closeAll({ 278 + (#C1 & #C2)... 279 + d: int 280 + }) 281 + // TODO: 282 + // The new semantics is more permissive here: 283 + // t1: v & {a: 1} @test(err, code=eval) 284 + // t2: v & {b: 1} @test(err, code=eval) 285 + 286 + t3: v & {c: 1} @test(eq, { 287 + c: 1 288 + d: int 289 + a?: _|_ 290 + b?: _|_ 291 + }) 292 + t4: v & {d: 1} @test(eq, { 293 + d: 1 294 + a?: _|_ 295 + c: int 296 + b?: _|_ 297 + }) 298 + t5: v & {e: 1} @test(err, code=eval) 299 + } 300 + 301 + // Disjunct → single embed, simplified. 302 + disjunct: { 303 + #A | _repo 304 + } 305 + 306 + // Parenthesized conjunct. 307 + parenConj: { 308 + (#A & _repo) 309 + } 310 + 311 + // Embedded close() with def arg → __closeAll wraps, close() left as-is. 312 + embeddedCloseOnly: { 313 + #A 314 + } 315 + 316 + // Embedded close() with def arg + extra field → __closeAll wraps. 317 + embeddedCloseField: { 318 + #A: a: int 319 + v: __closeAll({ 320 + #A... 321 + b: int 322 + }) 323 + 324 + t1: v & {a: 1} @test(eq, {a: 1, b: int}) 325 + t2: v & {b: 1} @test(eq, {b: 1, a: int}) 326 + t3: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 327 + } 328 + 329 + // Conjunction with extra field → not simplified, __closeAll wraps. 330 + conjunctField: __closeAll({ 331 + (#A & _repo)... 332 + b: int 333 + }) 334 + 335 + // Disjunction with extra field → not simplified, __reclose wraps. 336 + disjunctField: __reclose({ 337 + (#A | _repo)... 338 + b: int 339 + }) 340 + 341 + // Pure-definition disjunction → __closeAll (all operands are defs). 342 + disjunctDefs: { 343 + #A | #D 344 + } 345 + 346 + // and() with extra field → not simplified, __reclose wraps. 347 + andField: { 348 + #A: {a: int, b?: int} 349 + #D: {a: int, c?: int} 350 + v: __reclose({ 351 + and([#A, #D])... 352 + b: int 353 + }) 354 + 355 + t1: v & {a: 1} @test(eq, {a: 1, b: int, c?: _|_}) 356 + t2: v & {b: 1} @test(eq, {a: int, b: 1, c?: _|_}) 357 + 358 + // TODO: the new semantics is more permissive here: 359 + // t3: v & {c: 1} @test(err, code=eval, path=c, contains="field not allowed") 360 + } 361 + 362 + // or() with extra field → not simplified, __reclose wraps. 363 + orField: __reclose({ 364 + or([#A, #D])... 365 + b: int 366 + })
+147
tools/fix/testdata/explicitopen_shorthands.txtar
··· 1 + 2 + #exp:explicitopen 3 + 4 + # Test single-embed shorthands (where { expr } ≡ expr, so no wrapper needed) 5 + # and TODO comments generated inside comprehensions. 6 + 7 + -- cue.mod/module.cue -- 8 + module: "shorthands.test" 9 + 10 + language: version: "v0.15.0" 11 + 12 + -- in.cue -- 13 + package foo 14 + 15 + #A: a: int 16 + #D: x: int 17 + _repo: {} 18 + 19 + // Top-level definition embedding → opened with TODO. 20 + #A 21 + 22 + // Single definition embedding → use definition directly. 23 + singleDef: { 24 + #D 25 + } 26 + 27 + // Embedded and() call → single embed, used directly. 28 + embeddedAnd: { 29 + and([#A, #D]) 30 + } 31 + 32 + // Embedded or() call → single embed, used directly. 33 + embeddedOr: { 34 + or([#A, #D]) 35 + } 36 + 37 + // Embedded pkg.Func() call → no change (std lib doesn't return closed). 38 + embeddedPkgFunc: { 39 + len("foo") 40 + } 41 + 42 + // Embedded selector (e.g. foo.bar) → single embed, used directly. 43 + embeddedSel: { 44 + _repo.foo 45 + } 46 + 47 + // Definition in comprehension → TODO. 48 + compDef: { 49 + for v in _repo { 50 + #D 51 + } 52 + } 53 + 54 + // Non-def reference in comprehension → TODO. 55 + compRef: { 56 + for v in _repo { 57 + v 58 + } 59 + } 60 + 61 + // Comprehension inside a list. 62 + inList: { 63 + steps: [ 64 + for v in _repo.checkoutCode {v}, 65 + for v in _repo.installGo {v}, 66 + { 67 + name: "foo" 68 + }, 69 + ] 70 + } 71 + 72 + -- out/fixmod -- 73 + --- cue.mod/module.cue 74 + module: "shorthands.test" 75 + language: { 76 + version: "v0.15.0" 77 + } 78 + --- in.cue 79 + @experiment(explicitopen) 80 + 81 + package foo 82 + 83 + #A: a: int 84 + #D: x: int 85 + _repo: {} 86 + 87 + // Top-level definition embedding → opened with TODO. 88 + // TODO(cue-fix): top-level definition embedding opened; if this is intended as a schema, remove the '...'. 89 + #A... 90 + 91 + // Single definition embedding → use definition directly. 92 + singleDef: { 93 + #D 94 + } 95 + 96 + // Embedded and() call → single embed, used directly. 97 + embeddedAnd: { 98 + and([#A, #D]) 99 + } 100 + 101 + // Embedded or() call → single embed, used directly. 102 + embeddedOr: { 103 + or([#A, #D]) 104 + } 105 + 106 + // Embedded pkg.Func() call → no change (std lib doesn't return closed). 107 + embeddedPkgFunc: { 108 + len("foo") 109 + } 110 + 111 + // Embedded selector (e.g. foo.bar) → single embed, used directly. 112 + embeddedSel: { 113 + _repo.foo 114 + } 115 + 116 + // Definition in comprehension → TODO. 117 + compDef: __closeAll({ 118 + for v in _repo { 119 + // TODO(cue-fix): ... may not be intended inside a comprehension value; consider removing it. 120 + #D... 121 + } 122 + }) 123 + 124 + // Non-def reference in comprehension → TODO. 125 + compRef: __reclose({ 126 + for v in _repo { 127 + // TODO(cue-fix): ... may not be intended inside a comprehension value; consider removing it. 128 + v... 129 + } 130 + }) 131 + 132 + // Comprehension inside a list. 133 + inList: { 134 + steps: [ 135 + for v in _repo.checkoutCode { 136 + // TODO(cue-fix): ... may not be intended inside a comprehension value; consider removing it. 137 + v... 138 + }, 139 + for v in _repo.installGo { 140 + // TODO(cue-fix): ... may not be intended inside a comprehension value; consider removing it. 141 + v... 142 + }, 143 + { 144 + name: "foo" 145 + }, 146 + ] 147 + }
+6 -2
tools/fix/testdata/upgrade.txtar
··· 13 13 package foo 14 14 15 15 #A: a: int 16 + A: #A 16 17 17 18 X: { 18 - #A 19 + A 20 + b: int 19 21 } 20 22 -- out/fixmod -- 21 23 --- cue.mod/module.cue ··· 29 31 package foo 30 32 31 33 #A: a: int 34 + A: #A 32 35 33 36 X: __reclose({ 34 - #A... 37 + A... 38 + b: int 35 39 })