this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: extend shareID checks and fix path aliasing

- Fix appendPath: allocate a fresh selector slice so multiple
sibling calls from the same base path do not silently share
the same backing array (cue.Path.Append reuses excess capacity).
- Extend parseAtPath to handle integer segments (at=0 selects
list element 0).
- Extend extractShareIDsFromEqExpr to handle *ast.ListLit in
addition to *ast.StructLit.
- Extend collectDirectShareIDs to scan eq bodies for in-place
@test(shareID=...).
- Change typocheck @test(shareID=Z) to shareID:todo=Z: the v3
evaluator does not yet share Z and #Y vertices here.

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

+192 -55
+5 -5
cue/testdata/definitions/issue4000.txtar
··· 24 24 25 25 _items: [#VMRuleList] 26 26 27 - // TODO: this works, but make the in-place ones work as well for lists. 28 - // @test(shareID=L0, at=items.0) 29 - // @test(shareID=L1, at=items.1) 30 - // @test(shareID=L0, at=#VMRuleList.0) 31 - // @test(shareID=L1, at=#VMRuleList.1) 27 + // This is an alternative way of writing sharing. 28 + @test(shareID=L0, at=items.0) 29 + @test(shareID=L1, at=items.1) 30 + @test(shareID=L0, at=#VMRuleList.0) 31 + @test(shareID=L1, at=#VMRuleList.1) 32 32 33 33 #VMRuleList: [...null | #VMRule] & [{ 34 34 spec: [{
+2 -2
cue/testdata/definitions/typocheck.txtar
··· 44 44 @test(err, at=d.err, code=eval, contains="field not allowed", pos=[4:11]) 45 45 } 46 46 and: transitive: ok: { 47 - Z: {a: string} @test(shareID=Z) 48 - #Y: Z @test(shareID=Z) 47 + Z: {a: string} @test(shareID:todo=Z) 48 + #Y: Z @test(shareID:todo=Z) 49 49 #X: #Y & Z 50 50 out: #X & { 51 51 a: "foo"
+8
internal/cuetxtar/inline.go
··· 895 895 } 896 896 expectedKind |= k 897 897 } 898 + if pa.isTodo { 899 + if gotKind == expectedKind { 900 + t.Logf("WARNING: path %s: TODO kind:todo now passes — consider upgrading to @test(kind=%s)", path, expectedStr) 901 + } else { 902 + t.Logf("path %s: TODO kind:todo still failing: got kind %v, want %v", path, gotKind, expectedKind) 903 + } 904 + return 905 + } 898 906 if gotKind != expectedKind { 899 907 t.Errorf("path %s: @test(kind=%s): got kind %v, want %v", path, expectedStr, gotKind, expectedKind) 900 908 logHint(t, pa.hint)
+23 -7
internal/cuetxtar/inline_attr.go
··· 497 497 498 498 // appendPath appends a selector for label to path. 499 499 // hidPkg is forwarded to labelSelector; see its documentation. 500 + // 501 + // NOTE: a fresh slice is allocated intentionally so that multiple calls with 502 + // the same base do not share the same backing array. cue.Path.Append reuses 503 + // excess capacity, so callers that store the result and then append again from 504 + // the same base would silently overwrite each other's stored paths. 500 505 func appendPath(base cue.Path, label ast.Label, hidPkg string) cue.Path { 501 - return base.Append(labelSelector(label, hidPkg)) 506 + sels := base.Selectors() 507 + fresh := make([]cue.Selector, len(sels)+1) 508 + copy(fresh, sels) 509 + fresh[len(sels)] = labelSelector(label, hidPkg) 510 + return cue.MakePath(fresh...) 502 511 } 503 512 504 513 // parseAtPath parses an at= selector string into a cue.Path. 505 - // Unlike cue.ParsePath, it handles hidden field names with a $pkg qualifier 506 - // (e.g. "_foo$pkg" → cue.Hid("_foo", ":pkg"), matching the same syntax 507 - // accepted inside @test(eq, {...}) bodies). Dotted paths are split on "." and 508 - // each segment is processed independently, so "a._foo$pkg.b" works correctly. 514 + // Unlike cue.ParsePath, it handles: 515 + // - Hidden field names with a $pkg qualifier, e.g. "_foo$pkg" → 516 + // cue.Hid("_foo", ":pkg"), matching the syntax used inside @test(eq, ...) 517 + // bodies. 518 + // - Integer segments as list-index selectors, e.g. "items.0" → 519 + // [items, Index(0)]. 520 + // 521 + // Dotted paths are split on "." and each segment is processed independently, 522 + // so "a._foo$pkg.0" works correctly. 509 523 func parseAtPath(at string) (cue.Path, error) { 510 - // Fast path: if there are no hidden-field indicators, delegate directly. 511 - if !strings.Contains(at, "_") { 524 + // Fast path: no hidden fields and no integers — delegate directly. 525 + if !strings.Contains(at, "_") && !strings.ContainsAny(at, "0123456789") { 512 526 p := cue.ParsePath(at) 513 527 return p, p.Err() 514 528 } ··· 522 536 name = name[:i] 523 537 } 524 538 sels = append(sels, cue.Hid(name, pkg)) 539 + } else if n, err := strconv.Atoi(seg); err == nil { 540 + sels = append(sels, cue.Index(n)) 525 541 } else { 526 542 p := cue.ParsePath(seg) 527 543 if err := p.Err(); err != nil {
+93 -41
internal/cuetxtar/inline_shareid.go
··· 31 31 // Section 8: shareID — vertex sharing assertions 32 32 // ───────────────────────────────────────────────────────────────────────────── 33 33 34 - // extractShareIDsFromEqExpr walks the struct literal of an @test(eq, STRUCT) 35 - // body and collects all @test(shareID=name) annotations on fields. 36 - // basePath is the CUE path of the @test(eq) attribute; field paths in the 37 - // struct are appended to it. version is the active evaluator version name 38 - // used for version-specific share groups (@test(shareID=name)). 34 + // extractShareIDsFromEqExpr walks the expression of an @test(eq, EXPR) body 35 + // and collects all @test(shareID=name) annotations. 36 + // basePath is the CUE path of the @test(eq) attribute. 37 + // version is the active evaluator version for version-specific share groups. 38 + // 39 + // Supported expression forms: 40 + // - *ast.StructLit: @test(shareID=name) on fields → path = basePath.fieldLabel 41 + // - *ast.ListLit: @test(shareID=name) as decl attrs inside struct elements 42 + // → path = basePath.Index(i) 43 + // 39 44 // Returns a map from shareID name to the absolute paths of fields in that group. 40 45 func extractShareIDsFromEqExpr(expr ast.Expr, basePath cue.Path, version string) map[string][]cue.Path { 41 - s, ok := expr.(*ast.StructLit) 42 - if !ok { 43 - return nil 44 - } 45 46 var result map[string][]cue.Path 46 - for _, d := range s.Elts { 47 - f, ok := d.(*ast.Field) 48 - if !ok { 49 - continue 47 + addResult := func(name string, p cue.Path) { 48 + if result == nil { 49 + result = make(map[string][]cue.Path) 50 50 } 51 - for _, a := range f.Attrs { 51 + result[name] = append(result[name], p) 52 + } 53 + collectShareIDAttrs := func(attrs []*ast.Attribute, path cue.Path) { 54 + for _, a := range attrs { 52 55 if k, _ := a.Split(); k != "test" { 53 56 continue 54 57 } ··· 56 59 if err != nil || pa.directive != "shareID" { 57 60 continue 58 61 } 59 - // Version filter: skip if a non-matching version is specified. 60 62 if pa.version != "" && pa.version != version { 61 63 continue 62 64 } 63 65 if len(pa.raw.Fields) == 0 { 64 66 continue 65 67 } 66 - shareIDName := pa.raw.Fields[0].Value() 67 - if shareIDName == "" { 68 + name := pa.raw.Fields[0].Value() 69 + if name == "" { 68 70 continue 69 71 } 70 - fieldPath := applyShareIDAt(basePath.Append(labelSelector(f.Label, "")), pa) 71 - if result == nil { 72 - result = make(map[string][]cue.Path) 72 + addResult(name, applyShareIDAt(path, pa)) 73 + } 74 + } 75 + 76 + switch x := expr.(type) { 77 + case *ast.StructLit: 78 + // Struct body: look for @test(shareID=name) on fields. 79 + for _, d := range x.Elts { 80 + f, ok := d.(*ast.Field) 81 + if !ok { 82 + continue 73 83 } 74 - result[shareIDName] = append(result[shareIDName], fieldPath) 84 + collectShareIDAttrs(f.Attrs, basePath.Append(labelSelector(f.Label, ""))) 85 + } 86 + 87 + case *ast.ListLit: 88 + // List body: look for @test(shareID=name) as decl attrs inside 89 + // struct elements. The path for element i is basePath.Index(i). 90 + for i, elt := range x.Elts { 91 + s, ok := elt.(*ast.StructLit) 92 + if !ok { 93 + continue 94 + } 95 + elemPath := basePath.Append(cue.Index(i)) 96 + for _, d := range s.Elts { 97 + a, ok := d.(*ast.Attribute) 98 + if !ok { 99 + continue 100 + } 101 + collectShareIDAttrs([]*ast.Attribute{a}, elemPath) 102 + } 75 103 } 76 104 } 77 105 return result ··· 150 178 } 151 179 152 180 // collectDirectShareIDs builds a shareID group map from direct @test(shareID=name) 153 - // field attributes across ALL records at any nesting depth (no root filtering). 181 + // field attributes and in-place @test(shareID=name) inside @test(eq, ...) bodies, 182 + // across ALL records at any nesting depth (no root filtering). 154 183 // This is used for cross-root sharing assertions where fields from different 155 - // roots share a vertex. Eq-body sharing is handled per-root by 156 - // collectShareIDsForRoot. 184 + // roots share a vertex. 157 185 func (r *inlineRunner) collectDirectShareIDs(records []attrRecord, version string) map[string][]cue.Path { 186 + type attrKey struct { 187 + file string 188 + offset int 189 + } 158 190 var shareGroups map[string][]cue.Path 159 - for _, rec := range records { 160 - if rec.fileLevel { 161 - continue 191 + seenEq := make(map[attrKey]bool) 192 + add := func(id string, p cue.Path) { 193 + if shareGroups == nil { 194 + shareGroups = make(map[string][]cue.Path) 162 195 } 163 - 196 + shareGroups[id] = append(shareGroups[id], p) 197 + } 198 + for _, rec := range records { 164 199 pa := rec.parsed 165 200 if pa.version != "" && pa.version != version { 166 201 continue 167 202 } 168 - if pa.directive != "shareID" { 169 - continue 170 - } 171 - if len(pa.raw.Fields) == 0 { 172 - continue 173 - } 174 - shareIDName := pa.raw.Fields[0].Value() 175 - if shareIDName == "" { 176 - continue 177 - } 178 - if shareGroups == nil { 179 - shareGroups = make(map[string][]cue.Path) 203 + switch pa.directive { 204 + case "shareID": 205 + if len(pa.raw.Fields) == 0 { 206 + continue 207 + } 208 + shareIDName := pa.raw.Fields[0].Value() 209 + if shareIDName == "" { 210 + continue 211 + } 212 + add(shareIDName, applyShareIDAt(rec.path, pa)) 213 + 214 + case "eq": 215 + // Extract in-place @test(shareID=name) from fields in the eq body. 216 + if len(pa.raw.Fields) < 2 { 217 + continue 218 + } 219 + key := attrKey{file: pa.srcFileName, offset: pa.srcAttr.Pos().Offset()} 220 + if seenEq[key] { 221 + continue 222 + } 223 + seenEq[key] = true 224 + eqExpr, err := parser.ParseExpr("shareID", pa.raw.Fields[1].Text()) 225 + if err != nil { 226 + continue 227 + } 228 + for id, paths := range extractShareIDsFromEqExpr(eqExpr, rec.path, version) { 229 + for _, p := range paths { 230 + add(id, p) 231 + } 232 + } 180 233 } 181 - shareGroups[shareIDName] = append(shareGroups[shareIDName], applyShareIDAt(rec.path, pa)) 182 234 } 183 235 return shareGroups 184 236 }
+51
internal/cuetxtar/inline_test.go
··· 516 516 }) 517 517 } 518 518 519 + // TestShareIDPathAliasing is a regression test for a cue.Path backing-array 520 + // aliasing bug in appendPath. When a parent path has excess backing capacity, 521 + // cue.Path.Append reuses the same underlying array for all children, so a 522 + // later sibling's append overwrites position len(parent) for all previously 523 + // stored paths. The result: every @test(shareID=...) record for the same 524 + // parent ends up with the path of the last child visited. 525 + // 526 + // The test verifies that collectShareIDsForRoot records the correct path for 527 + // each annotated sibling — not the last one — by checking that the collected 528 + // paths match the expected field names. 529 + func TestShareIDPathAliasing(t *testing.T) { 530 + // The aliasing requires the parent path to have excess backing capacity. 531 + // Go doubles slice capacity on growth: len 2 → cap 4 at the third append. 532 + // So a path of length 3 (e.g. "p.q.r") has cap 4, meaning all children 533 + // of "p.q.r" share backing-array index 3 without the fix. 534 + // 535 + // Four siblings under "p.q.r". @test(shareID=AB) is on "a" and "b" (not 536 + // the last field "d"), so without the fix both records end up with path 537 + // "p.q.r.d" instead of "p.q.r.a" / "p.q.r.b". 538 + src := `p: q: r: { 539 + a: {x: 1} @test(shareID=AB) 540 + b: a @test(shareID=AB) 541 + c: {x: 2} 542 + d: {x: 3} 543 + }` 544 + f, err := parser.ParseFile("test.cue", src, parser.ParseComments) 545 + if err != nil { 546 + t.Fatal(err) 547 + } 548 + records := extractTestAttrs(f, "test.cue") 549 + 550 + r := &inlineRunner{} 551 + rootPath := cue.MakePath(cue.Str("p"), cue.Str("q"), cue.Str("r")) 552 + groups := r.collectShareIDsForRoot(records, rootPath, "v3") 553 + 554 + paths, ok := groups["AB"] 555 + if !ok { 556 + t.Fatal("shareID group 'AB' not found") 557 + } 558 + if len(paths) != 2 { 559 + t.Fatalf("expected 2 paths in group AB, got %d: %v", len(paths), paths) 560 + } 561 + want := []string{"p.q.r.a", "p.q.r.b"} 562 + got := []string{paths[0].String(), paths[1].String()} 563 + if !slices.Equal(got, want) { 564 + t.Errorf("path aliasing: got %v, want %v\n"+ 565 + "(if both paths show 'root.d', appendPath is aliasing sibling paths)", 566 + got, want) 567 + } 568 + } 569 + 519 570 // TestAtDirective verifies that @test(err, at=<path>, ...) navigates to a 520 571 // sub-path before checking the error. 521 572 func TestAtDirective(t *testing.T) {
+10
internal/cuetxtar/inlinerunner_test.go
··· 185 185 runExpectPass(t, "-- test.cue --\nx: 42 @test(err:todo, p=1, code=eval)\n") 186 186 }) 187 187 188 + t.Run("kind:todo still failing does not fail test", func(t *testing.T) { 189 + // @test(kind:todo=int) where kind is float: no test failure. 190 + runExpectPass(t, "-- test.cue --\nx: 1.0 @test(kind:todo=int)\n") 191 + }) 192 + 193 + t.Run("kind:todo passing does not fail test", func(t *testing.T) { 194 + // @test(kind:todo=int) where kind matches: logs a warning but no failure. 195 + runExpectPass(t, "-- test.cue --\nx: int @test(kind:todo=int)\n") 196 + }) 197 + 188 198 t.Run("eq incorrect passing logs note", func(t *testing.T) { 189 199 // @test(eq, X, incorrect) where value matches X: suppresses "this is wrong" 190 200 // feeling by logging a NOTE, but does not fail.