| // Copyright 2018 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // Package debug provides tools to print a parsed expression graph and | |
| // adorn each expression element with additional metadata. | |
| package debug | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "strconv" | |
| "strings" | |
| "cel.dev/cel-go/common/ast" | |
| "cel.dev/cel-go/common/types" | |
| "cel.dev/cel-go/common/types/ref" | |
| ) | |
| // Adorner returns debug metadata that will be tacked on to the string | |
| // representation of an expression. | |
| type Adorner interface { | |
| // GetMetadata for the input context. | |
| GetMetadata(ctx any) string | |
| } | |
| // Writer manages writing expressions to an internal string. | |
| type Writer interface { | |
| fmt.Stringer | |
| // Buffer pushes an expression into an internal queue of expressions to | |
| // write to a string. | |
| Buffer(e ast.Expr) | |
| } | |
| type emptyDebugAdorner struct { | |
| } | |
| var emptyAdorner Adorner = &emptyDebugAdorner{} | |
| func (a *emptyDebugAdorner) GetMetadata(e any) string { | |
| return "" | |
| } | |
| // ToDebugString gives the unadorned string representation of the Expr. | |
| func ToDebugString(e ast.Expr) string { | |
| return ToAdornedDebugString(e, emptyAdorner) | |
| } | |
| // ToAdornedDebugString gives the adorned string representation of the Expr. | |
| func ToAdornedDebugString(e ast.Expr, adorner Adorner) string { | |
| w := newDebugWriter(adorner) | |
| w.Buffer(e) | |
| return w.String() | |
| } | |
| // debugWriter is used to print out pretty-printed debug strings. | |
| type debugWriter struct { | |
| adorner Adorner | |
| buffer bytes.Buffer | |
| indent int | |
| lineStart bool | |
| } | |
| func newDebugWriter(a Adorner) *debugWriter { | |
| return &debugWriter{ | |
| adorner: a, | |
| indent: 0, | |
| lineStart: true, | |
| } | |
| } | |
| func (w *debugWriter) Buffer(e ast.Expr) { | |
| if e == nil { | |
| return | |
| } | |
| switch e.Kind() { | |
| case ast.LiteralKind: | |
| w.append(formatLiteral(e.AsLiteral())) | |
| case ast.IdentKind: | |
| w.append(e.AsIdent()) | |
| case ast.SelectKind: | |
| w.appendSelect(e.AsSelect()) | |
| case ast.CallKind: | |
| w.appendCall(e.AsCall()) | |
| case ast.ListKind: | |
| w.appendList(e.AsList()) | |
| case ast.MapKind: | |
| w.appendMap(e.AsMap()) | |
| case ast.StructKind: | |
| w.appendStruct(e.AsStruct()) | |
| case ast.ComprehensionKind: | |
| w.appendComprehension(e.AsComprehension()) | |
| } | |
| w.adorn(e) | |
| } | |
| func (w *debugWriter) appendSelect(sel ast.SelectExpr) { | |
| w.Buffer(sel.Operand()) | |
| w.append(".") | |
| w.append(sel.FieldName()) | |
| if sel.IsTestOnly() { | |
| w.append("~test-only~") | |
| } | |
| } | |
| func (w *debugWriter) appendCall(call ast.CallExpr) { | |
| if call.IsMemberFunction() { | |
| w.Buffer(call.Target()) | |
| w.append(".") | |
| } | |
| w.append(call.FunctionName()) | |
| w.append("(") | |
| if len(call.Args()) > 0 { | |
| w.addIndent() | |
| w.appendLine() | |
| for i, arg := range call.Args() { | |
| if i > 0 { | |
| w.append(",") | |
| w.appendLine() | |
| } | |
| w.Buffer(arg) | |
| } | |
| w.removeIndent() | |
| w.appendLine() | |
| } | |
| w.append(")") | |
| } | |
| func (w *debugWriter) appendList(list ast.ListExpr) { | |
| w.append("[") | |
| if len(list.Elements()) > 0 { | |
| w.appendLine() | |
| w.addIndent() | |
| for i, elem := range list.Elements() { | |
| if i > 0 { | |
| w.append(",") | |
| w.appendLine() | |
| } | |
| w.Buffer(elem) | |
| } | |
| w.removeIndent() | |
| w.appendLine() | |
| } | |
| w.append("]") | |
| } | |
| func (w *debugWriter) appendStruct(obj ast.StructExpr) { | |
| w.append(obj.TypeName()) | |
| w.append("{") | |
| if len(obj.Fields()) > 0 { | |
| w.appendLine() | |
| w.addIndent() | |
| for i, f := range obj.Fields() { | |
| field := f.AsStructField() | |
| if i > 0 { | |
| w.append(",") | |
| w.appendLine() | |
| } | |
| if field.IsOptional() { | |
| w.append("?") | |
| } | |
| w.append(field.Name()) | |
| w.append(":") | |
| w.Buffer(field.Value()) | |
| w.adorn(f) | |
| } | |
| w.removeIndent() | |
| w.appendLine() | |
| } | |
| w.append("}") | |
| } | |
| func (w *debugWriter) appendMap(m ast.MapExpr) { | |
| w.append("{") | |
| if m.Size() > 0 { | |
| w.appendLine() | |
| w.addIndent() | |
| for i, e := range m.Entries() { | |
| entry := e.AsMapEntry() | |
| if i > 0 { | |
| w.append(",") | |
| w.appendLine() | |
| } | |
| if entry.IsOptional() { | |
| w.append("?") | |
| } | |
| w.Buffer(entry.Key()) | |
| w.append(":") | |
| w.Buffer(entry.Value()) | |
| w.adorn(e) | |
| } | |
| w.removeIndent() | |
| w.appendLine() | |
| } | |
| w.append("}") | |
| } | |
| func (w *debugWriter) appendComprehension(comprehension ast.ComprehensionExpr) { | |
| w.append("__comprehension__(") | |
| w.addIndent() | |
| w.appendLine() | |
| w.append("// Variable") | |
| w.appendLine() | |
| w.append(comprehension.IterVar()) | |
| w.append(",") | |
| w.appendLine() | |
| if comprehension.HasIterVar2() { | |
| w.append(comprehension.IterVar2()) | |
| w.append(",") | |
| w.appendLine() | |
| } | |
| w.append("// Target") | |
| w.appendLine() | |
| w.Buffer(comprehension.IterRange()) | |
| w.append(",") | |
| w.appendLine() | |
| w.append("// Accumulator") | |
| w.appendLine() | |
| w.append(comprehension.AccuVar()) | |
| w.append(",") | |
| w.appendLine() | |
| w.append("// Init") | |
| w.appendLine() | |
| w.Buffer(comprehension.AccuInit()) | |
| w.append(",") | |
| w.appendLine() | |
| w.append("// LoopCondition") | |
| w.appendLine() | |
| w.Buffer(comprehension.LoopCondition()) | |
| w.append(",") | |
| w.appendLine() | |
| w.append("// LoopStep") | |
| w.appendLine() | |
| w.Buffer(comprehension.LoopStep()) | |
| w.append(",") | |
| w.appendLine() | |
| w.append("// Result") | |
| w.appendLine() | |
| w.Buffer(comprehension.Result()) | |
| w.append(")") | |
| w.removeIndent() | |
| } | |
| func formatLiteral(c ref.Val) string { | |
| switch v := c.(type) { | |
| case types.Bool: | |
| return fmt.Sprintf("%t", v) | |
| case types.Bytes: | |
| return fmt.Sprintf("b%s", strconv.Quote(string(v))) | |
| case types.Double: | |
| return fmt.Sprintf("%v", float64(v)) | |
| case types.Int: | |
| return fmt.Sprintf("%d", int64(v)) | |
| case types.String: | |
| return strconv.Quote(string(v)) | |
| case types.Uint: | |
| return fmt.Sprintf("%du", uint64(v)) | |
| case types.Null: | |
| return "null" | |
| default: | |
| panic("Unknown constant type") | |
| } | |
| } | |
| func (w *debugWriter) append(s string) { | |
| w.doIndent() | |
| w.buffer.WriteString(s) | |
| } | |
| func (w *debugWriter) appendFormat(f string, args ...any) { | |
| w.append(fmt.Sprintf(f, args...)) | |
| } | |
| func (w *debugWriter) doIndent() { | |
| if w.lineStart { | |
| w.lineStart = false | |
| w.buffer.WriteString(strings.Repeat(" ", w.indent)) | |
| } | |
| } | |
| func (w *debugWriter) adorn(e any) { | |
| w.append(w.adorner.GetMetadata(e)) | |
| } | |
| func (w *debugWriter) appendLine() { | |
| w.buffer.WriteString("\n") | |
| w.lineStart = true | |
| } | |
| func (w *debugWriter) addIndent() { | |
| w.indent++ | |
| } | |
| func (w *debugWriter) removeIndent() { | |
| w.indent-- | |
| if w.indent < 0 { | |
| panic("negative indent") | |
| } | |
| } | |
| func (w *debugWriter) String() string { | |
| return w.buffer.String() | |
| } | |
| type idAdorner struct{} | |
| func (a *idAdorner) GetMetadata(elem any) string { | |
| e, isExpr := elem.(ast.Expr) | |
| if !isExpr { | |
| return "" | |
| } | |
| return fmt.Sprintf("@id:%d ", e.ID()) | |
| } | |
| // ToDebugStringWithIDs returns a string representation with AST node IDs. | |
| func ToDebugStringWithIDs(e ast.Expr) string { | |
| return ToAdornedDebugString(e, &idAdorner{}) | |
| } |