this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: improve @test(eq) formatting and body validation

CUE_UPDATE=1 formatting improvements for @test(eq):
- Compact form: when a multi-line formatted value is < 20 characters it
is collapsed to a single line (e.g. "{a: 1}" instead of "{\n\ta: 1\n}").
- Indentation: multi-line values longer than 20 characters are re-indented
relative to the @test attribute's source line, matching the same trick
already used by @test(debug). This avoids both over-indentation and
mis-alignment when the attribute is deeply nested.

Body validation: report @test(...) field attributes inside an @test(eq, ...)
body that have no effect there. Only the directives that astCmp actively
processes (final, ignore, err, shareID, checkOrder) are permitted; anything
else is almost certainly misplaced and is now flagged as a test error.

Also fix a stale comment: the shareID version suffix example was using
":v3" which is optional; the updated wording makes clear that a bare
"shareID=name" is the default form.

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

+96 -15
+3 -3
CLAUDE.md
··· 163 163 wrong; move it inside the struct as a trailing decl attribute instead. 164 164 165 165 13. **Structure sharing (`~(field)`)**: When the `out/evalalpha` section shows a 166 - field as a shared reference (e.g., `y: ~(x)`), add `@test(shareID:v3=name)` to 166 + field as a shared reference (e.g., `y: ~(x)`), add `@test(shareID=name)` to 167 167 both the referencing field (`y`) and the referenced field (`x`), using the same 168 168 name to assert they share the same underlying vertex. Use the `:v3` version suffix 169 169 when the sharing is v3-specific (i.e., v2 expanded the field into an independent 170 170 struct). Example: 171 171 ``` 172 - x: a & {b: 1} @test(eq, {a: 1, b: 1}) @test(shareID:v3=xy) 173 - y: x @test(eq, {a: 1, b: 1}) @test(shareID:v3=xy) 172 + x: a & {b: 1} @test(eq, {a: 1, b: 1}) @test(shareID=xy) 173 + y: x @test(eq, {a: 1, b: 1}) @test(shareID=xy) 174 174 ``` 175 175 176 176 ### Contribution Model
+2 -4
cue/testdata/fulleval/030_retain_references_with_interleaved_embedding.txtar
··· 11 11 }) @test(closed) 12 12 13 13 #base: { 14 - #info: {...} 15 - } @test(eq, { 16 - #info: {} @test(closed) 17 - }) 14 + #info: {...} @test(closed) 15 + } @test(eq, { #info: {} }) 18 16 19 17 a: [Name=string]: {#info: { 20 18 X: "foo"
+91 -8
internal/cuetxtar/inline.go
··· 225 225 f0 := attr.Fields[0] 226 226 if f0.Key() != "" { 227 227 dir := f0.Key() 228 - // Key-based directives may carry a version suffix: "shareID:v3" → directive="shareID", version="v3". 228 + // Key-based directives may carry a version suffix: "shareID" → directive="shareID", version="v3". 229 229 if idx := strings.LastIndex(dir, ":"); idx >= 0 { 230 230 result.directive = dir[:idx] 231 231 result.version = dir[idx+1:] ··· 1280 1280 if exprStr == "" { 1281 1281 // Empty @test(eq) or @test(eq, at=N) — fill placeholder. 1282 1282 if cuetest.UpdateGoldenFiles { 1283 - r.enqueueInlineFill(pa, r.eqFillAttr(val, atStr)) 1283 + r.enqueueInlineFill(pa, r.eqFillAttr(val, atStr, pa)) 1284 1284 } 1285 1285 return 1286 1286 } ··· 1289 1289 t.Errorf("path %s: @test(eq, ...): cannot parse expected expression: %v", path, err) 1290 1290 return 1291 1291 } 1292 + 1293 + // Detect any @test(...) field attributes inside the eq body — they have no 1294 + // effect there and are almost certainly misplaced. 1295 + reportEqBodyTestAttrs(t, path, expr) 1292 1296 1293 1297 // Detect stale-skip: an existing skip:<ver> positional arg on this attr 1294 1298 // marks a known discrepancy recorded by a prior manual annotation. ··· 1300 1304 if hasSkip && cuetest.UpdateGoldenFiles { 1301 1305 // Stale-skip cleanup: the assertion now passes; strip the skip, 1302 1306 // restoring @test(eq, <expr>[, at=<sel>]). 1303 - r.enqueueInlineFill(pa, r.eqFillAttrStr(exprStr, atStr)) 1307 + r.enqueueInlineFill(pa, r.eqFillAttrStr(exprStr, atStr, pa)) 1304 1308 } 1305 1309 return 1306 1310 } ··· 1308 1312 // Comparison failed — genuine mismatch. 1309 1313 if cuetest.ForceUpdateGoldenFiles { 1310 1314 // CUE_UPDATE=force: overwrite the assertion with the actual value. 1311 - r.enqueueInlineFill(pa, r.eqFillAttr(val, atStr)) 1315 + r.enqueueInlineFill(pa, r.eqFillAttr(val, atStr, pa)) 1312 1316 return 1313 1317 } 1314 1318 // Report the failure (unless already annotated with a skip). ··· 1319 1323 } 1320 1324 1321 1325 // eqFillAttr builds an @test(eq, <value>[, at=<atStr>]) attribute for fill/force-update. 1322 - func (r *inlineRunner) eqFillAttr(v cue.Value, atStr string) string { 1326 + func (r *inlineRunner) eqFillAttr(v cue.Value, atStr string, pa parsedTestAttr) string { 1323 1327 if r.isError(v) { 1324 1328 return "@test(err)" 1325 1329 } 1326 - return r.eqFillAttrStr(r.formatValue(v), atStr) 1330 + return r.eqFillAttrStr(r.formatValue(v), atStr, pa) 1327 1331 } 1328 1332 1329 1333 // eqFillAttrStr builds @test(eq, <exprStr>[, at=<atStr>]). 1330 - func (r *inlineRunner) eqFillAttrStr(exprStr, atStr string) string { 1334 + // For multi-line expressions, if the compact single-line form is < 20 chars it 1335 + // is used directly; otherwise lines after the first are re-indented using the 1336 + // leading whitespace of the source line containing the @test attribute (the 1337 + // same offset trick as formatDebugAttr, but without an extra tab because 1338 + // format.Node already carries one tab of relative indentation). 1339 + func (r *inlineRunner) eqFillAttrStr(exprStr, atStr string, pa parsedTestAttr) string { 1340 + if strings.Contains(exprStr, "\n") { 1341 + if compact := compactCUEExpr(exprStr); len(compact) < 20 { 1342 + exprStr = compact 1343 + } else { 1344 + indent := r.attrLineIndent(pa) 1345 + exprStr = strings.ReplaceAll(exprStr, "\n", "\n"+indent) 1346 + } 1347 + } 1331 1348 if atStr != "" { 1332 1349 return fmt.Sprintf("@test(eq, %s, at=%s)", exprStr, atStr) 1333 1350 } 1334 1351 return fmt.Sprintf("@test(eq, %s)", exprStr) 1335 1352 } 1336 1353 1354 + // compactCUEExpr collapses a multi-line CUE expression produced by format.Node 1355 + // into a single line. It handles struct and list literals by joining their 1356 + // tab-indented field lines with ", ". Only safe for shallow (non-nested) 1357 + // structures; deeply-nested values will exceed the 20-char threshold and use 1358 + // the indented form instead. 1359 + func compactCUEExpr(s string) string { 1360 + lines := strings.Split(s, "\n") 1361 + parts := make([]string, 0, len(lines)) 1362 + for _, line := range lines { 1363 + trimmed := strings.TrimLeft(line, "\t") 1364 + if trimmed != "" { 1365 + parts = append(parts, trimmed) 1366 + } 1367 + } 1368 + if len(parts) < 2 { 1369 + return strings.Join(parts, "") 1370 + } 1371 + open, close_ := parts[0], parts[len(parts)-1] 1372 + middle := parts[1 : len(parts)-1] 1373 + if (open == "{" || open == "[") && len(middle) > 0 { 1374 + return open + strings.Join(middle, ", ") + close_ 1375 + } 1376 + return strings.Join(parts, " ") 1377 + } 1378 + 1337 1379 // formatValue returns a human-readable CUE string for a value. 1338 1380 // Routes through the Vertex export path (via cue.Final()) to avoid internal 1339 1381 // _#def wrapping, then re-enables optional fields (value?: T) so the ··· 1391 1433 elts = append(elts, e) 1392 1434 } 1393 1435 sl.Elts = elts 1436 + return true 1437 + }, nil) 1438 + } 1439 + 1440 + // eqBodySupportedDirectives lists the @test directive names that are 1441 + // intentionally processed by astCmp when they appear inside an @test(eq, ...) 1442 + // body (as field-level attributes or struct-level decl attributes). 1443 + // Any other directive has no effect there. 1444 + var eqBodySupportedDirectives = map[string]bool{ 1445 + "final": true, // field-level and struct-level: resolve default before comparing 1446 + "ignore": true, // field-level: skip eq descent; field need not exist 1447 + "err": true, // field-level: check that value is an error 1448 + "shareID": true, // field-level: sharing assertion (handled by extractShareIDsFromEqExpr) 1449 + "checkOrder": true, // struct-level decl: require fields in declaration order 1450 + } 1451 + 1452 + // reportEqBodyTestAttrs walks the expected expression of an @test(eq, ...) 1453 + // body and reports any @test field attributes that have no effect there. 1454 + // Directives listed in eqBodySupportedDirectives are intentionally processed 1455 + // by astCmp and are excluded from the error. 1456 + func reportEqBodyTestAttrs(t testing.TB, path cue.Path, expr ast.Node) { 1457 + t.Helper() 1458 + ast.Walk(expr, func(n ast.Node) bool { 1459 + f, ok := n.(*ast.Field) 1460 + if !ok { 1461 + return true 1462 + } 1463 + for _, a := range f.Attrs { 1464 + k, _ := a.Split() 1465 + if k != "test" { 1466 + continue 1467 + } 1468 + pa, err := parseTestAttr(a) 1469 + if err != nil { 1470 + continue 1471 + } 1472 + if eqBodySupportedDirectives[pa.directive] { 1473 + continue 1474 + } 1475 + t.Errorf("path %s: @test(%s) in @test(eq, ...) body has no effect; place it as a field attribute on the actual value", path, pa.directive) 1476 + } 1394 1477 return true 1395 1478 }, nil) 1396 1479 } ··· 1693 1776 // body and collects all @test(shareID=name) annotations on fields. 1694 1777 // basePath is the CUE path of the @test(eq) attribute; field paths in the 1695 1778 // struct are appended to it. version is the active evaluator version name 1696 - // used for version-specific share groups (@test(shareID:v3=name)). 1779 + // used for version-specific share groups (@test(shareID=name)). 1697 1780 // Returns a map from shareID name to the absolute paths of fields in that group. 1698 1781 func extractShareIDsFromEqExpr(expr ast.Expr, basePath cue.Path, version string) map[string][]cue.Path { 1699 1782 s, ok := expr.(*ast.StructLit)