lib/dump.go (125 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 lib import ( "bytes" "encoding/json" "fmt" "sort" "strings" "github.com/google/cel-go/cel" "github.com/google/cel-go/common" ) // NewDump returns an evaluation dump that can be used to examine the complete // set of evaluation states from a CEL program. The program must have been // constructed with a cel.Env.Program call including the cel.OptTrackState // evaluation option. The ast and details parameters must be valid for the // program. func NewDump(ast *cel.Ast, details *cel.EvalDetails) *Dump { if ast == nil || details == nil { return nil } return &Dump{ast: ast, det: details} } // Dump is an evaluation dump. type Dump struct { ast *cel.Ast det *cel.EvalDetails } func (d *Dump) String() string { if d == nil { return "" } var buf strings.Builder for i, v := range d.NodeValues() { if i != 0 { buf.WriteByte('\n') } fmt.Fprint(&buf, v) } return buf.String() } // NodeValues returns the evaluation results, source location and source // snippets for the expressions in the dump. The nodes are sorted in // source order. func (d *Dump) NodeValues() []NodeValue { if d == nil { return nil } es := d.det.State() var values []NodeValue for _, id := range es.IDs() { if id == 0 { continue } v, ok := es.Value(id) if !ok { continue } values = append(values, d.nodeValue(v, id)) } sort.Slice(values, func(i, j int) bool { vi := values[i].loc vj := values[j].loc switch { case vi.Line() < vj.Line(): return true case vi.Line() > vj.Line(): return false } switch { case vi.Column() < vj.Column(): return true case vi.Column() > vj.Column(): return false default: // If we are here we have executed more than once // and have different values, so sort lexically. // This is not ideal given that values may include // maps which do not render consistently and so // we're breaking the sort invariant that comparisons // will be consistent. For what we are doing this is // good enough. return fmt.Sprint(values[i].val) < fmt.Sprint(values[j].val) } }) return values } func (d *Dump) nodeValue(val any, id int64) NodeValue { v := NodeValue{ loc: d.ast.NativeRep().SourceInfo().GetStartLocation(id), src: d.ast.Source(), val: val, } return v } // NodeValue is a CEL expression node value and annotation. type NodeValue struct { loc common.Location src common.Source val any } func (v NodeValue) MarshalJSON() ([]byte, error) { type val struct { Location string `json:"loc"` Src string `json:"src"` Val any `json:"val"` } var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) err := enc.Encode(val{ Location: v.Loc(), Src: v.Src(), Val: v.val, }) if err != nil { return nil, err } return buf.Bytes(), nil } func (v NodeValue) String() string { return fmt.Sprintf("%s\n%s\n%v\n", v.Loc(), v.Src(), v.Val()) } func (v NodeValue) Val() any { return v.val } func (v NodeValue) Loc() string { return fmt.Sprintf("%s:%d:%d", v.src.Description(), v.loc.Line(), v.loc.Column()+1) } func (v NodeValue) Src() string { snippet, ok := v.src.Snippet(v.loc.Line()) if !ok { return "" } src := " | " + strings.Replace(snippet, "\t", " ", -1) ind := "\n | " + strings.Repeat(".", minInt(v.loc.Column(), len(snippet))) + "^" return src + ind } func minInt(a, b int) int { if a < b { return a } return b }