Skip to content

Commit ae3ad16

Browse files
committed
internal/astinternal: revise API with more options
Add two options which will soon be useful. First, an OmitEmpty boolean option, as invalid or empty lines like the ones below are usually not helpful: Optional: token.Pos("-") Constraint: token.Token("ILLEGAL") Attrs: []*ast.Attribute{} Second, add a Filter func option which gives flexibility in terms of what Go values we are interested in. For example, the TOML tests will soon use this to only print token.Pos values. For #3379. Signed-off-by: Daniel Martí <[email protected]> Change-Id: Iaa1af9f987d68b3cafeaaece2ef697e2fa2b7678 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1200204 Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Matthew Sackman <[email protected]> TryBot-Result: CUEcueckoo <[email protected]>
1 parent 710b438 commit ae3ad16

File tree

7 files changed

+442
-56
lines changed

7 files changed

+442
-56
lines changed

internal/astinternal/debug.go

Lines changed: 93 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -27,45 +27,59 @@ import (
2727
"cuelang.org/go/internal"
2828
)
2929

30-
// DebugPrint writes a multi-line Go-like representation of a syntax tree node,
30+
// AppendDebug writes a multi-line Go-like representation of a syntax tree node,
3131
// including node position information and any relevant Go types.
32-
//
33-
// Note that since this is an internal debugging API, [io.Writer] errors are ignored,
34-
// as it is assumed that the caller is using a [bytes.Buffer] or directly
35-
// writing to standard output.
36-
func DebugPrint(w io.Writer, node ast.Node) {
37-
d := &debugPrinter{w: w}
38-
d.value(reflect.ValueOf(node), nil)
39-
d.newline()
32+
func AppendDebug(dst []byte, node ast.Node, config DebugConfig) []byte {
33+
d := &debugPrinter{cfg: config}
34+
dst = d.value(dst, reflect.ValueOf(node), nil)
35+
dst = d.newline(dst)
36+
return dst
37+
}
38+
39+
// DebugConfig configures the behavior of [AppendDebug].
40+
type DebugConfig struct {
41+
// Filter is called before each value in a syntax tree.
42+
// Values for which the function returns false are omitted.
43+
Filter func(reflect.Value) bool
44+
45+
// OmitEmpty causes empty strings, empty structs, empty lists,
46+
// nil pointers, invalid positions, and missing tokens to be omitted.
47+
OmitEmpty bool
4048
}
4149

4250
type debugPrinter struct {
4351
w io.Writer
52+
cfg DebugConfig
4453
level int
4554
}
4655

47-
func (d *debugPrinter) printf(format string, args ...any) {
48-
fmt.Fprintf(d.w, format, args...)
56+
func (d *debugPrinter) printf(dst []byte, format string, args ...any) []byte {
57+
return fmt.Appendf(dst, format, args...)
4958
}
5059

51-
func (d *debugPrinter) newline() {
52-
fmt.Fprintf(d.w, "\n%s", strings.Repeat("\t", d.level))
60+
func (d *debugPrinter) newline(dst []byte) []byte {
61+
return fmt.Appendf(dst, "\n%s", strings.Repeat("\t", d.level))
5362
}
5463

5564
var (
5665
typeTokenPos = reflect.TypeFor[token.Pos]()
5766
typeTokenToken = reflect.TypeFor[token.Token]()
5867
)
5968

60-
func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
69+
func (d *debugPrinter) value(dst []byte, v reflect.Value, impliedType reflect.Type) []byte {
70+
if d.cfg.Filter != nil && !d.cfg.Filter(v) {
71+
return dst
72+
}
6173
// Skip over interface types.
6274
if v.Kind() == reflect.Interface {
6375
v = v.Elem()
6476
}
6577
// Indirecting a nil interface gives a zero value.
6678
if !v.IsValid() {
67-
d.printf("nil")
68-
return
79+
if !d.cfg.OmitEmpty {
80+
dst = d.printf(dst, "nil")
81+
}
82+
return dst
6983
}
7084

7185
// We print the original pointer type if there was one.
@@ -74,54 +88,72 @@ func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
7488
v = reflect.Indirect(v)
7589
// Indirecting a nil pointer gives a zero value.
7690
if !v.IsValid() {
77-
d.printf("nil")
78-
return
91+
if !d.cfg.OmitEmpty {
92+
dst = d.printf(dst, "nil")
93+
}
94+
return dst
95+
}
96+
97+
if d.cfg.OmitEmpty && v.IsZero() {
98+
return dst
7999
}
80100

81101
t := v.Type()
82102
switch t {
83103
// Simple types which can stringify themselves.
84104
case typeTokenPos, typeTokenToken:
85-
d.printf("%s(%q)", t, v)
86-
return
105+
dst = d.printf(dst, "%s(%q)", t, v)
106+
return dst
87107
}
88108

109+
undoValue := len(dst)
89110
switch t.Kind() {
90111
default:
91112
// We assume all other kinds are basic in practice, like string or bool.
92113
if t.PkgPath() != "" {
93114
// Mention defined and non-predeclared types, for clarity.
94-
d.printf("%s(%#v)", t, v)
115+
dst = d.printf(dst, "%s(%#v)", t, v)
95116
} else {
96-
d.printf("%#v", v)
117+
dst = d.printf(dst, "%#v", v)
97118
}
98119

99120
case reflect.Slice:
100121
if origType != impliedType {
101-
d.printf("%s", origType)
102-
}
103-
d.printf("{")
104-
if v.Len() > 0 {
105-
d.level++
106-
for i := 0; i < v.Len(); i++ {
107-
d.newline()
108-
ev := v.Index(i)
109-
// Note: a slice literal implies the type of its elements
110-
// so we can avoid mentioning the type
111-
// of each element if it matches.
112-
d.value(ev, t.Elem())
122+
dst = d.printf(dst, "%s", origType)
123+
}
124+
dst = d.printf(dst, "{")
125+
d.level++
126+
anyElems := false
127+
for i := 0; i < v.Len(); i++ {
128+
ev := v.Index(i)
129+
undoElem := len(dst)
130+
dst = d.newline(dst)
131+
// Note: a slice literal implies the type of its elements
132+
// so we can avoid mentioning the type
133+
// of each element if it matches.
134+
if dst2 := d.value(dst, ev, t.Elem()); len(dst2) == len(dst) {
135+
dst = dst[:undoElem]
136+
} else {
137+
dst = dst2
138+
anyElems = true
113139
}
114-
d.level--
115-
d.newline()
116140
}
117-
d.printf("}")
141+
d.level--
142+
if !anyElems && d.cfg.OmitEmpty {
143+
dst = dst[:undoValue]
144+
} else {
145+
if anyElems {
146+
dst = d.newline(dst)
147+
}
148+
dst = d.printf(dst, "}")
149+
}
118150

119151
case reflect.Struct:
120152
if origType != impliedType {
121-
d.printf("%s", origType)
153+
dst = d.printf(dst, "%s", origType)
122154
}
123-
d.printf("{")
124-
printed := false
155+
dst = d.printf(dst, "{")
156+
anyElems := false
125157
d.level++
126158
for i := 0; i < v.NumField(); i++ {
127159
f := t.Field(i)
@@ -133,28 +165,38 @@ func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
133165
case "Scope", "Node", "Unresolved":
134166
continue
135167
}
136-
printed = true
137-
d.newline()
138-
d.printf("%s: ", f.Name)
139-
d.value(v.Field(i), nil)
168+
undoElem := len(dst)
169+
dst = d.newline(dst)
170+
dst = d.printf(dst, "%s: ", f.Name)
171+
if dst2 := d.value(dst, v.Field(i), nil); len(dst2) == len(dst) {
172+
dst = dst[:undoElem]
173+
} else {
174+
dst = dst2
175+
anyElems = true
176+
}
140177
}
141178
val := v.Addr().Interface()
142179
if val, ok := val.(ast.Node); ok {
143180
// Comments attached to a node aren't a regular field, but are still useful.
144181
// The majority of nodes won't have comments, so skip them when empty.
145182
if comments := ast.Comments(val); len(comments) > 0 {
146-
printed = true
147-
d.newline()
148-
d.printf("Comments: ")
149-
d.value(reflect.ValueOf(comments), nil)
183+
anyElems = true
184+
dst = d.newline(dst)
185+
dst = d.printf(dst, "Comments: ")
186+
dst = d.value(dst, reflect.ValueOf(comments), nil)
150187
}
151188
}
152189
d.level--
153-
if printed {
154-
d.newline()
190+
if !anyElems && d.cfg.OmitEmpty {
191+
dst = dst[:undoValue]
192+
} else {
193+
if anyElems {
194+
dst = d.newline(dst)
195+
}
196+
dst = d.printf(dst, "}")
155197
}
156-
d.printf("}")
157198
}
199+
return dst
158200
}
159201

160202
func DebugStr(x interface{}) (out string) {

internal/astinternal/debug_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
package astinternal_test
1616

1717
import (
18+
"path"
19+
"reflect"
1820
"strings"
1921
"testing"
2022

23+
"cuelang.org/go/cue/ast"
2124
"cuelang.org/go/cue/parser"
2225
"cuelang.org/go/internal/astinternal"
2326
"cuelang.org/go/internal/cuetxtar"
@@ -39,8 +42,29 @@ func TestDebugPrint(t *testing.T) {
3942
f, err := parser.ParseFile(file.Name, file.Data, parser.ParseComments)
4043
qt.Assert(t, qt.IsNil(err))
4144

42-
w := t.Writer(file.Name)
43-
astinternal.DebugPrint(w, f)
45+
// The full syntax tree, as printed by default.
46+
full := astinternal.AppendDebug(nil, f, astinternal.DebugConfig{})
47+
t.Writer(file.Name).Write(full)
48+
49+
// A syntax tree which omits any empty values,
50+
// and is only interested in showing string fields.
51+
// We allow ast.Nodes and slices to not stop too early.
52+
typNode := reflect.TypeFor[ast.Node]()
53+
strings := astinternal.AppendDebug(nil, f, astinternal.DebugConfig{
54+
OmitEmpty: true,
55+
Filter: func(v reflect.Value) bool {
56+
if v.Type().Implements(typNode) {
57+
return true
58+
}
59+
switch v.Kind() {
60+
case reflect.Slice, reflect.String:
61+
return true
62+
default:
63+
return false
64+
}
65+
},
66+
})
67+
t.Writer(path.Join(file.Name, "omitempty-strings")).Write(strings)
4468
}
4569
})
4670
}

internal/astinternal/testdata/debugprint/comprehensions.txtar

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,69 @@ for k, v in input if v > 2 {
107107
}
108108
Imports: []*ast.ImportSpec{}
109109
}
110+
-- out/debugprint/comprehensions.cue/omitempty-strings --
111+
*ast.File{
112+
Filename: "comprehensions.cue"
113+
Decls: []ast.Decl{
114+
*ast.Comprehension{
115+
Clauses: []ast.Clause{
116+
*ast.IfClause{
117+
Condition: *ast.Ident{
118+
Name: "condition"
119+
}
120+
}
121+
}
122+
Value: *ast.StructLit{
123+
Elts: []ast.Decl{
124+
*ast.Field{
125+
Label: *ast.Ident{
126+
Name: "a"
127+
}
128+
Value: *ast.BasicLit{
129+
Value: "true"
130+
}
131+
}
132+
}
133+
}
134+
}
135+
*ast.Comprehension{
136+
Clauses: []ast.Clause{
137+
*ast.ForClause{
138+
Key: *ast.Ident{
139+
Name: "k"
140+
}
141+
Value: *ast.Ident{
142+
Name: "v"
143+
}
144+
Source: *ast.Ident{
145+
Name: "input"
146+
}
147+
}
148+
*ast.IfClause{
149+
Condition: *ast.BinaryExpr{
150+
X: *ast.Ident{
151+
Name: "v"
152+
}
153+
Y: *ast.BasicLit{
154+
Value: "2"
155+
}
156+
}
157+
}
158+
}
159+
Value: *ast.StructLit{
160+
Elts: []ast.Decl{
161+
*ast.Field{
162+
Label: *ast.ParenExpr{
163+
X: *ast.Ident{
164+
Name: "k"
165+
}
166+
}
167+
Value: *ast.Ident{
168+
Name: "v"
169+
}
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)