Skip to content

Commit

Permalink
internal/astinternal: add DebugPrint from cue-ast-print
Browse files Browse the repository at this point in the history
As an internal API next to the existing DebugStr, which appears to be
significantly less verbose. I want to start using this API in packages
such as encoding/toml's tests to ensure that the decoded nodes
contain all the relevant position information.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
Change-Id: I5a84b4319b8e5e15129da603d81e826007647425
Dispatch-Trailer: {"type":"trybot","CL":1199934,"patchset":2,"ref":"refs/changes/34/1199934/2","targetBranch":"master"}
  • Loading branch information
mvdan authored and cueckoo committed Aug 27, 2024
1 parent 9f620c2 commit 7bb3f47
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 127 deletions.
128 changes: 128 additions & 0 deletions internal/astinternal/debugstr.go → internal/astinternal/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ package astinternal

import (
"fmt"
gotoken "go/token"
"io"
"reflect"
"strconv"
"strings"

Expand All @@ -24,6 +27,131 @@ import (
"cuelang.org/go/internal"
)

// DebugPrint 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()
}

type debugPrinter struct {
w io.Writer
level int
}

func (d *debugPrinter) printf(format string, args ...any) {
fmt.Fprintf(d.w, format, args...)
}

func (d *debugPrinter) newline() {
fmt.Fprintf(d.w, "\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) {
// Skip over interface types.
if v.Kind() == reflect.Interface {
v = v.Elem()
}

// Indirecting a nil interface/pointer gives a zero value;
// stop as calling reflect.Value.Type on an invalid type would panic.
if !v.IsValid() {
d.printf("nil")
return
}
// We print the original pointer type if there was one.
origType := v.Type()
v = reflect.Indirect(v)

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

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)
} else {
d.printf("%#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())
}
d.level--
d.newline()
}
d.printf("}")

case reflect.Struct:
if origType != impliedType {
d.printf("%s", origType)
}
d.printf("{")
printed := false
d.level++
for i := 0; i < v.NumField(); i++ {
f := t.Field(i)
if !gotoken.IsExported(f.Name) {
continue
}
switch f.Name {
// These fields are cyclic, and they don't represent the syntax anyway.
case "Scope", "Node", "Unresolved":
continue
}
printed = true
d.newline()
d.printf("%s: ", f.Name)
d.value(v.Field(i), nil)
}
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)
}
}
d.level--
if printed {
d.newline()
}
d.printf("}")
}
}

func DebugStr(x interface{}) (out string) {
if n, ok := x.(ast.Node); ok {
comments := ""
Expand Down
132 changes: 5 additions & 127 deletions internal/cmd/cue-ast-print/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ package main

import (
"flag"
"log"
"os"

"fmt"
gotoken "go/token"
"io"
"reflect"
"strings"
"log"
"os"

"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal/astinternal"
)

func main() {
Expand All @@ -56,124 +51,7 @@ func main() {
}
file, err := parser.ParseFile(filename, src, parser.ParseComments)
if err != nil {
panic(err)
}
debugPrint(os.Stdout, file)
}

func debugPrint(w io.Writer, node ast.Node) {
d := &debugPrinter{w: w}
d.value(reflect.ValueOf(node), nil)
d.newline()
}

type debugPrinter struct {
w io.Writer
level int
}

func (d *debugPrinter) printf(format string, args ...any) {
fmt.Fprintf(d.w, format, args...)
}

func (d *debugPrinter) newline() {
fmt.Fprintf(d.w, "\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) {
// Skip over interface types.
if v.Kind() == reflect.Interface {
v = v.Elem()
}

// Indirecting a nil interface/pointer gives a zero value;
// stop as calling reflect.Value.Type on an invalid type would panic.
if !v.IsValid() {
d.printf("nil")
return
}
// We print the original pointer type if there was one.
origType := v.Type()
v = reflect.Indirect(v)

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

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)
} else {
d.printf("%#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())
}
d.level--
d.newline()
}
d.printf("}")
case reflect.Struct:
if origType != impliedType {
d.printf("%s", origType)
}
d.printf("{")
printed := false
d.level++
for i := 0; i < v.NumField(); i++ {
f := t.Field(i)
if !gotoken.IsExported(f.Name) {
continue
}
switch f.Name {
// These fields are cyclic, and they don't represent the syntax anyway.
case "Scope", "Node", "Unresolved":
continue
}
printed = true
d.newline()
d.printf("%s: ", f.Name)
d.value(v.Field(i), nil)
}
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)
}
}
d.level--
if printed {
d.newline()
}
d.printf("}")
log.Fatal(err)
}
astinternal.DebugPrint(os.Stdout, file)
}

0 comments on commit 7bb3f47

Please sign in to comment.