Skip to content

Commit

Permalink
internal/astinternal: revise API with more options
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
mvdan committed Aug 29, 2024
1 parent 710b438 commit ae3ad16
Show file tree
Hide file tree
Showing 7 changed files with 442 additions and 56 deletions.
144 changes: 93 additions & 51 deletions internal/astinternal/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,45 +27,59 @@ import (
"cuelang.org/go/internal"
)

// DebugPrint writes a multi-line Go-like representation of a syntax tree node,
// AppendDebug writes a multi-line Go-like representation of a syntax tree node,
// including node position information and any relevant Go types.
//
// Note that since this is an internal debugging API, [io.Writer] errors are ignored,
// as it is assumed that the caller is using a [bytes.Buffer] or directly
// writing to standard output.
func DebugPrint(w io.Writer, node ast.Node) {
d := &debugPrinter{w: w}
d.value(reflect.ValueOf(node), nil)
d.newline()
func AppendDebug(dst []byte, node ast.Node, config DebugConfig) []byte {
d := &debugPrinter{cfg: config}
dst = d.value(dst, reflect.ValueOf(node), nil)
dst = d.newline(dst)
return dst
}

// DebugConfig configures the behavior of [AppendDebug].
type DebugConfig struct {
// Filter is called before each value in a syntax tree.
// Values for which the function returns false are omitted.
Filter func(reflect.Value) bool

// OmitEmpty causes empty strings, empty structs, empty lists,
// nil pointers, invalid positions, and missing tokens to be omitted.
OmitEmpty bool
}

type debugPrinter struct {
w io.Writer
cfg DebugConfig
level int
}

func (d *debugPrinter) printf(format string, args ...any) {
fmt.Fprintf(d.w, format, args...)
func (d *debugPrinter) printf(dst []byte, format string, args ...any) []byte {
return fmt.Appendf(dst, format, args...)
}

func (d *debugPrinter) newline() {
fmt.Fprintf(d.w, "\n%s", strings.Repeat("\t", d.level))
func (d *debugPrinter) newline(dst []byte) []byte {
return fmt.Appendf(dst, "\n%s", strings.Repeat("\t", d.level))
}

var (
typeTokenPos = reflect.TypeFor[token.Pos]()
typeTokenToken = reflect.TypeFor[token.Token]()
)

func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
func (d *debugPrinter) value(dst []byte, v reflect.Value, impliedType reflect.Type) []byte {
if d.cfg.Filter != nil && !d.cfg.Filter(v) {
return dst
}
// Skip over interface types.
if v.Kind() == reflect.Interface {
v = v.Elem()
}
// Indirecting a nil interface gives a zero value.
if !v.IsValid() {
d.printf("nil")
return
if !d.cfg.OmitEmpty {
dst = d.printf(dst, "nil")
}
return dst
}

// We print the original pointer type if there was one.
Expand All @@ -74,54 +88,72 @@ func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
v = reflect.Indirect(v)
// Indirecting a nil pointer gives a zero value.
if !v.IsValid() {
d.printf("nil")
return
if !d.cfg.OmitEmpty {
dst = d.printf(dst, "nil")
}
return dst
}

if d.cfg.OmitEmpty && v.IsZero() {
return dst
}

t := v.Type()
switch t {
// Simple types which can stringify themselves.
case typeTokenPos, typeTokenToken:
d.printf("%s(%q)", t, v)
return
dst = d.printf(dst, "%s(%q)", t, v)
return dst
}

undoValue := len(dst)
switch t.Kind() {
default:
// We assume all other kinds are basic in practice, like string or bool.
if t.PkgPath() != "" {
// Mention defined and non-predeclared types, for clarity.
d.printf("%s(%#v)", t, v)
dst = d.printf(dst, "%s(%#v)", t, v)
} else {
d.printf("%#v", v)
dst = d.printf(dst, "%#v", v)
}

case reflect.Slice:
if origType != impliedType {
d.printf("%s", origType)
}
d.printf("{")
if v.Len() > 0 {
d.level++
for i := 0; i < v.Len(); i++ {
d.newline()
ev := v.Index(i)
// Note: a slice literal implies the type of its elements
// so we can avoid mentioning the type
// of each element if it matches.
d.value(ev, t.Elem())
dst = d.printf(dst, "%s", origType)
}
dst = d.printf(dst, "{")
d.level++
anyElems := false
for i := 0; i < v.Len(); i++ {
ev := v.Index(i)
undoElem := len(dst)
dst = d.newline(dst)
// Note: a slice literal implies the type of its elements
// so we can avoid mentioning the type
// of each element if it matches.
if dst2 := d.value(dst, ev, t.Elem()); len(dst2) == len(dst) {
dst = dst[:undoElem]
} else {
dst = dst2
anyElems = true
}
d.level--
d.newline()
}
d.printf("}")
d.level--
if !anyElems && d.cfg.OmitEmpty {
dst = dst[:undoValue]
} else {
if anyElems {
dst = d.newline(dst)
}
dst = d.printf(dst, "}")
}

case reflect.Struct:
if origType != impliedType {
d.printf("%s", origType)
dst = d.printf(dst, "%s", origType)
}
d.printf("{")
printed := false
dst = d.printf(dst, "{")
anyElems := false
d.level++
for i := 0; i < v.NumField(); i++ {
f := t.Field(i)
Expand All @@ -133,28 +165,38 @@ func (d *debugPrinter) value(v reflect.Value, impliedType reflect.Type) {
case "Scope", "Node", "Unresolved":
continue
}
printed = true
d.newline()
d.printf("%s: ", f.Name)
d.value(v.Field(i), nil)
undoElem := len(dst)
dst = d.newline(dst)
dst = d.printf(dst, "%s: ", f.Name)
if dst2 := d.value(dst, v.Field(i), nil); len(dst2) == len(dst) {
dst = dst[:undoElem]
} else {
dst = dst2
anyElems = true
}
}
val := v.Addr().Interface()
if val, ok := val.(ast.Node); ok {
// Comments attached to a node aren't a regular field, but are still useful.
// The majority of nodes won't have comments, so skip them when empty.
if comments := ast.Comments(val); len(comments) > 0 {
printed = true
d.newline()
d.printf("Comments: ")
d.value(reflect.ValueOf(comments), nil)
anyElems = true
dst = d.newline(dst)
dst = d.printf(dst, "Comments: ")
dst = d.value(dst, reflect.ValueOf(comments), nil)
}
}
d.level--
if printed {
d.newline()
if !anyElems && d.cfg.OmitEmpty {
dst = dst[:undoValue]
} else {
if anyElems {
dst = d.newline(dst)
}
dst = d.printf(dst, "}")
}
d.printf("}")
}
return dst
}

func DebugStr(x interface{}) (out string) {
Expand Down
28 changes: 26 additions & 2 deletions internal/astinternal/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
package astinternal_test

import (
"path"
"reflect"
"strings"
"testing"

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

w := t.Writer(file.Name)
astinternal.DebugPrint(w, f)
// The full syntax tree, as printed by default.
full := astinternal.AppendDebug(nil, f, astinternal.DebugConfig{})
t.Writer(file.Name).Write(full)

// A syntax tree which omits any empty values,
// and is only interested in showing string fields.
// We allow ast.Nodes and slices to not stop too early.
typNode := reflect.TypeFor[ast.Node]()
strings := astinternal.AppendDebug(nil, f, astinternal.DebugConfig{
OmitEmpty: true,
Filter: func(v reflect.Value) bool {
if v.Type().Implements(typNode) {
return true
}
switch v.Kind() {
case reflect.Slice, reflect.String:
return true
default:
return false
}
},
})
t.Writer(path.Join(file.Name, "omitempty-strings")).Write(strings)
}
})
}
66 changes: 66 additions & 0 deletions internal/astinternal/testdata/debugprint/comprehensions.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,69 @@ for k, v in input if v > 2 {
}
Imports: []*ast.ImportSpec{}
}
-- out/debugprint/comprehensions.cue/omitempty-strings --
*ast.File{
Filename: "comprehensions.cue"
Decls: []ast.Decl{
*ast.Comprehension{
Clauses: []ast.Clause{
*ast.IfClause{
Condition: *ast.Ident{
Name: "condition"
}
}
}
Value: *ast.StructLit{
Elts: []ast.Decl{
*ast.Field{
Label: *ast.Ident{
Name: "a"
}
Value: *ast.BasicLit{
Value: "true"
}
}
}
}
}
*ast.Comprehension{
Clauses: []ast.Clause{
*ast.ForClause{
Key: *ast.Ident{
Name: "k"
}
Value: *ast.Ident{
Name: "v"
}
Source: *ast.Ident{
Name: "input"
}
}
*ast.IfClause{
Condition: *ast.BinaryExpr{
X: *ast.Ident{
Name: "v"
}
Y: *ast.BasicLit{
Value: "2"
}
}
}
}
Value: *ast.StructLit{
Elts: []ast.Decl{
*ast.Field{
Label: *ast.ParenExpr{
X: *ast.Ident{
Name: "k"
}
}
Value: *ast.Ident{
Name: "v"
}
}
}
}
}
}
}
Loading

0 comments on commit ae3ad16

Please sign in to comment.