this repo has no description
0
fork

Configure Feed

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

internal/cuetxtar: fill nested pos=[]

CUE_UPDATE=1 now fills pos=[] placeholders
in @test(err) directives that appear inside
an @test(eq, {...}) body, matching the
behaviour of top-level @test(err, pos=[]).

- Add posWriteback callback to cmpCtx so
cmpErr can enqueue fill write-backs
without direct runner access.
- Add enqueueNestedPosWrite to locate the
inner attr text within the outer attr
and rewrite pos=[...] in-place.
- Refactor formatPosSpec to take explicit
baseLine and srcFileName params, enabling
reuse from both code paths.
- Extract replacePosSpec (using
strings.Cut) to DRY up pos=[...]
find-and-replace across enqueuePosWrite,
enqueueNestedPosWrite, and
replaceSuberrPos.
- Emit _|_ for error values in
eqWriteValue to avoid a confusing
let-containing struct in fill output.
- Add BottomLit check in astCmp so _|_
requires val.Err() != nil.
- Convert 039_reference_to_root.txtar to
use @test(eq) annotations.

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

+123 -46
+6 -5
cue/testdata/resolve/039_reference_to_root.txtar
··· 3 3 c: a & { 4 4 b: 100 5 5 d: a.b + 3 // do not resolve as c != a. 6 - } @test(eq, {b: 100, d: _|_}) // d should be incomplete, not 103. 6 + } @test(eq, {b: 100, d: _|_ @test(err, 7 + code=incomplete, contains="non-concrete value int in operand to +", pos=[4:5, 1:8])}) 7 8 x: { 8 9 b: int 9 10 c: b + 5 @test(err, code=incomplete, contains="non-concrete value int in operand to +", pos=[0:5, -1:5]) ··· 22 23 ./in.cue:4:5 23 24 ./in.cue:1:8 24 25 [incomplete] x.c: non-concrete value int in operand to +: 26 + ./in.cue:9:5 25 27 ./in.cue:8:5 26 - ./in.cue:7:5 27 28 [incomplete] v.c: non-concrete value int in operand to +: 29 + ./in.cue:15:5 28 30 ./in.cue:14:5 29 - ./in.cue:13:5 30 31 [incomplete] w.c: non-concrete value int in operand to +: 32 + ./in.cue:15:5 31 33 ./in.cue:14:5 32 - ./in.cue:13:5 33 34 [incomplete] wp.c: non-concrete value int in operand to +: 35 + ./in.cue:15:5 34 36 ./in.cue:14:5 35 - ./in.cue:13:5 36 37 -- out/compile -- 37 38 --- in.cue 38 39 {
+23 -6
internal/cuetxtar/astcmp.go
··· 26 26 "cuelang.org/go/internal" 27 27 "cuelang.org/go/internal/core/adt" 28 28 "cuelang.org/go/internal/core/export" 29 + "cuelang.org/go/internal/cuetest" 29 30 "cuelang.org/go/internal/value" 30 31 ) 31 32 32 33 // cmpCtx carries comparison options through the recursive astCmp calls. 33 34 type cmpCtx struct { 34 - // baseLine is the 1-indexed source line used to resolve relative pos= 35 - // specs in nested @test(err) directives. When 0, deltaLine values in 36 - // pos specs are treated as absolute line numbers. 35 + // baseLine resolves pos= specs in nested @test(err) directives: 36 + // wantLine = baseLine + deltaLine. runEqInline keeps it 0, so deltaLine 37 + // values in nested pos= specs are absolute 1-indexed line numbers. This 38 + // differs from top-level @test(err) where pa.baseLine makes deltaLine a 39 + // relative offset; changing it here would break existing specs. 37 40 baseLine int 41 + // posWriteback, when non-nil, is called by cmpErr when it encounters a 42 + // pos=[] placeholder inside an @test(eq, {...}) body. The runner uses it 43 + // to locate and fill that placeholder in the source attribute. 44 + // innerAttrText is the raw text of the inner @test(err,...) attribute 45 + // (used to locate it within the outer @test(eq,...) source text). 46 + posWriteback func(innerAttrText string, positions []token.Pos) 38 47 } 39 48 40 49 // astCompare compares a parsed CUE AST expression against an evaluated ··· 102 111 case *ast.ParenExpr: 103 112 return c.astCmp(path, e.X, val) 104 113 case *ast.BottomLit: 114 + if val.Err() == nil { 115 + return pathErr(path, "_|_: expected error, got %v", val) 116 + } 105 117 return nil 106 118 case *ast.CallExpr, *ast.SelectorExpr: 107 119 return cmpBuiltinExpr(path, expr, val) ··· 297 309 if errCk == nil { 298 310 errCk = &errArgs{} // bare @test(err) 299 311 } 312 + errCk.srcAttrText = a.Text 300 313 case "shareID": 301 314 // The first field with a given shareID name runs the eq check 302 315 // normally so every value-expr pair is checked at least once. ··· 649 662 return pathErr(path, "@test(err): expected error code %v, got %q", ea.codes, gotCode) 650 663 } 651 664 } 652 - // TODO: support CUE_UPDATE=1 and CUE_UPDATE=force for nested pos= specs. 653 - // This requires rewriting text inside the outer @test(eq, {...}) attribute 654 - // body, which the current enqueuePosWrite machinery doesn't handle. 655 665 if ea.posSet { 656 666 err := val.Err() 657 667 if err == nil { 658 668 return pathErr(path, "@test(err, pos=...): value has no error") 659 669 } 660 670 positions := cueerrors.Positions(err) 671 + isPlaceholder := len(ea.pos) == 0 672 + // pos=[] is a fill-in placeholder: update with CUE_UPDATE=1, same as 673 + // the top-level @test(err, pos=[]) path in checkErrPositions. 674 + if ((isPlaceholder && cuetest.UpdateGoldenFiles) || cuetest.ForceUpdateGoldenFiles) && c.posWriteback != nil && ea.srcAttrText != "" { 675 + c.posWriteback(ea.srcAttrText, positions) 676 + return nil 677 + } 661 678 if len(positions) != len(ea.pos) { 662 679 var got []string 663 680 for _, p := range positions {
+8 -1
internal/cuetxtar/inline.go
··· 52 52 "cuelang.org/go/cue/format" 53 53 "cuelang.org/go/cue/load" 54 54 "cuelang.org/go/cue/parser" 55 + "cuelang.org/go/cue/token" 55 56 "cuelang.org/go/internal" 56 57 "cuelang.org/go/internal/cuetdtest" 57 58 "cuelang.org/go/internal/cuetest" ··· 825 826 // marks a known discrepancy recorded by a prior manual annotation. 826 827 _, hasSkip := attrHasSkip(pa.raw) 827 828 828 - cmpErr := (&cmpCtx{baseLine: 0}).astCmp(cue.Path{}, expr, val) 829 + ctx := &cmpCtx{ 830 + baseLine: 0, // nested pos= specs use absolute line numbers (deltaLine == absLine) 831 + posWriteback: func(innerAttrText string, positions []token.Pos) { 832 + r.enqueueNestedPosWrite(pa, innerAttrText, positions) 833 + }, 834 + } 835 + cmpErr := ctx.astCmp(cue.Path{}, expr, val) 829 836 if cmpErr == nil { 830 837 // Assertion passes via AST comparison. 831 838 if hasSkip && cuetest.UpdateGoldenFiles {
+64 -33
internal/cuetxtar/inline_err.go
··· 82 82 // msgArgs holds expected fmt.Sprint representations of Msg() args to check 83 83 // order-independently against the error's Msg() arguments. 84 84 msgArgs []string 85 + // srcAttrText is the raw text of the @test(err,...) attribute as it 86 + // appears in source (e.g. "@test(err, code=eval, pos=[])"). Set only for 87 + // @test(err) attributes that appear inside an @test(eq, {...}) body; used 88 + // by cmpErr to locate and rewrite a pos=[] placeholder in the outer 89 + // @test(eq,...) attribute text. 90 + srcAttrText string 85 91 } 86 92 87 93 // matchesCode reports whether the given error code satisfies the codes ··· 560 566 } 561 567 } 562 568 563 - // enqueueSubErrPosWrites applies all sub-error position updates atomically to 564 - // the source attribute, producing a single posWrite entry. Each update replaces 565 - // pos=[...] in the expIdx-th suberr=(...) group. 566 569 // formatPosSpec converts a single token.Pos to a position spec string. 567 - // Positions in the same file as the @test attribute are written as 568 - // deltaLine:col (relative to pa.baseLine); positions in other files are 569 - // written as filename:absLine:col (absolute). 570 - func (r *inlineRunner) formatPosSpec(p token.Pos, pa parsedTestAttr) string { 571 - if p.Filename() == "" || r.relFilename(p.Filename()) == pa.srcFileName { 572 - return fmt.Sprintf("%d:%d", p.Line()-pa.baseLine, p.Column()) 570 + // Positions in the same file are written as deltaLine:col (relative to 571 + // baseLine); positions in other files are written as filename:absLine:col. 572 + // Pass pa.baseLine and pa.srcFileName for top-level @test(err) directives 573 + // (relative delta form). Pass 0 for baseLine to produce absolute line 574 + // numbers (used for nested @test(err) inside @test(eq) bodies). 575 + func (r *inlineRunner) formatPosSpec(p token.Pos, baseLine int, srcFileName string) string { 576 + if p.Filename() == "" || r.relFilename(p.Filename()) == srcFileName { 577 + return fmt.Sprintf("%d:%d", p.Line()-baseLine, p.Column()) 573 578 } 574 579 return fmt.Sprintf("%s:%d:%d", r.relFilename(p.Filename()), p.Line(), p.Column()) 575 580 } 576 581 582 + // replacePosSpec replaces the content of the first pos=[...] found in text 583 + // starting at offset with newContent. Returns (newText, true) on success, or 584 + // (text, false) when no pos=[...] bracket pair is found. 585 + func replacePosSpec(text string, offset int, newContent string) (string, bool) { 586 + prefix, rest, found := strings.Cut(text[offset:], "pos=[") 587 + if !found { 588 + return text, false 589 + } 590 + _, suffix, found := strings.Cut(rest, "]") 591 + if !found { 592 + return text, false 593 + } 594 + return text[:offset] + prefix + "pos=[" + newContent + "]" + suffix, true 595 + } 596 + 597 + // enqueueNestedPosWrite fills a pos=[] placeholder inside an @test(eq, {...}) 598 + // body. innerAttrText is the raw text of the inner @test(err,...) attribute 599 + // (e.g. "@test(err, code=eval, pos=[])"); it is located by substring search 600 + // within the outer @test(eq,...) attribute text and the pos=[...] within it is 601 + // replaced with the formatted positions. 602 + func (r *inlineRunner) enqueueNestedPosWrite(outerPa parsedTestAttr, innerAttrText string, positions []token.Pos) { 603 + outerText := outerPa.srcAttr.Text 604 + innerIdx := strings.Index(outerText, innerAttrText) 605 + if innerIdx < 0 { 606 + return // inner attr not found in outer text — skip 607 + } 608 + // Use baseLine=0 so specs are absolute line numbers, matching the 609 + // cmpCtx.baseLine=0 convention used when checking nested pos= in cmpErr. 610 + parts := make([]string, len(positions)) 611 + for i, p := range positions { 612 + parts[i] = r.formatPosSpec(p, 0, outerPa.srcFileName) 613 + } 614 + newAttrText, ok := replacePosSpec(outerText, innerIdx, strings.Join(parts, ", ")) 615 + if !ok { 616 + return 617 + } 618 + r.pendingPosWrites = append(r.pendingPosWrites, posWrite{ 619 + fileName: outerPa.srcFileName, 620 + attrOffset: outerPa.srcAttr.Pos().Offset(), 621 + attrLen: len(outerPa.srcAttr.Text), 622 + newAttrText: newAttrText, 623 + }) 624 + } 625 + 577 626 func (r *inlineRunner) enqueueSubErrPosWrites(pa parsedTestAttr, updates []posUpdate) { 578 627 newAttrText := pa.srcAttr.Text 579 628 // Apply updates from highest expIdx to lowest so earlier indices stay valid. ··· 583 632 for _, u := range updates { 584 633 parts := make([]string, len(u.positions)) 585 634 for i, p := range u.positions { 586 - parts[i] = r.formatPosSpec(p, pa) 635 + parts[i] = r.formatPosSpec(p, pa.baseLine, pa.srcFileName) 587 636 } 588 637 newPosStr := strings.Join(parts, ", ") 589 638 newAttrText = replaceSuberrPos(newAttrText, u.expIdx, newPosStr) ··· 629 678 } 630 679 // attrText[innerStart:end] is the content inside suberr=(...). 631 680 inner := attrText[innerStart:end] 632 - posIdx := strings.Index(inner, "pos=[") 633 - if posIdx < 0 { 681 + newInner, ok := replacePosSpec(inner, 0, newPosContent) 682 + if !ok { 634 683 return attrText // no pos= in this suberr group 635 684 } 636 - bracket := posIdx + len("pos=[") 637 - closeIdx := strings.Index(inner[bracket:], "]") 638 - if closeIdx < 0 { 639 - return attrText 640 - } 641 - closeIdx += bracket + 1 // include "]" 642 - newInner := inner[:posIdx] + "pos=[" + newPosContent + "]" + inner[closeIdx:] 643 685 return attrText[:innerStart] + newInner + attrText[end:] 644 686 } 645 687 ··· 800 842 func (r *inlineRunner) enqueuePosWrite(pa parsedTestAttr, positions []token.Pos) { 801 843 parts := make([]string, len(positions)) 802 844 for i, p := range positions { 803 - parts[i] = r.formatPosSpec(p, pa) 845 + parts[i] = r.formatPosSpec(p, pa.baseLine, pa.srcFileName) 804 846 } 805 - newPosStr := strings.Join(parts, ", ") 806 - 807 - old := pa.srcAttr.Text 808 - start := strings.Index(old, "pos=[") 809 - if start < 0 { 847 + newAttrText, ok := replacePosSpec(pa.srcAttr.Text, 0, strings.Join(parts, ", ")) 848 + if !ok { 810 849 return 811 850 } 812 - bracket := start + len("pos=[") 813 - end := strings.Index(old[bracket:], "]") 814 - if end < 0 { 815 - return 816 - } 817 - end += bracket + 1 // include the "]" 818 - newAttrText := old[:start] + "pos=[" + newPosStr + "]" + old[end:] 819 - 820 851 r.pendingPosWrites = append(r.pendingPosWrites, posWrite{ 821 852 fileName: pa.srcFileName, 822 853 attrOffset: pa.srcAttr.Pos().Offset(),
+10 -1
internal/cuetxtar/inline_format.go
··· 153 153 return 154 154 } 155 155 156 + // Error/incomplete values (e.g. int + 3 where int is abstract) — emit _|_. 157 + // This avoids the confusing let-containing struct that v.Syntax(Final()) 158 + // generates when it tries to make the expression self-contained. 159 + // astCmp requires _|_ to match only error values. 160 + if v.Err() != nil { 161 + b.WriteString("_|_") 162 + return 163 + } 164 + 156 165 // Scalar values and lists: fall back to the standard syntax formatter. 157 166 // cue.Final() resolves defaults and avoids _#def wrapping. 158 - syn := v.Syntax(cue.Docs(false), cue.Final(), cue.Optional(true)) 167 + syn := v.Syntax(cue.Docs(false), cue.Final(), cue.Optional(true), cue.Raw()) 159 168 stripComments(syn) 160 169 bs, err := format.Node(syn, format.Simplify()) 161 170 if err != nil {
+12
internal/cuetxtar/inlinerunner_test.go
··· 61 61 name: "eq passes for selector (math.Pi)", 62 62 archive: "-- test.cue --\nimport \"math\"\nx: math.Pi @test(eq, math.Pi)\n", 63 63 }, 64 + { 65 + // Incomplete field (int + 3 where int is abstract) must match _|_ and 66 + // must not generate a let-containing struct in the fill output. 67 + name: "eq matches _|_ for incomplete field in struct", 68 + archive: "-- test.cue --\na: {b: int}\nc: a & {\n\tb: 100\n\td: a.b + 3\n} @test(eq, {b: 100, d: _|_})\n", 69 + }, 70 + { 71 + // @test(err) inside @test(eq) body can carry filled pos= specs. 72 + // pos=[3:5] means absolute line 3, col 5 (baseLine=0 convention). 73 + name: "eq with nested @test(err, pos=) passes", 74 + archive: "-- test.cue --\na: {b: int}\nc: a & {\n\tb: 100\n\td: a.b + 3\n} @test(eq, {b: 100, d: _|_ @test(err, code=incomplete, pos=[4:5, 1:8])})\n", 75 + }, 64 76 } 65 77 66 78 for _, tt := range tests {