From b64e1b232520a96f83a8b2d1010d15547ec30828 Mon Sep 17 00:00:00 2001 From: Rohan Patnaik Date: Thu, 11 Jun 2026 14:31:48 +0530 Subject: [PATCH] feat: expose issue error offsets --- cel/env.go | 35 +++++++++++++++++++++++++++++++++++ cel/env_test.go | 32 ++++++++++++++++++++++++++++++++ common/errors.go | 5 +++++ 3 files changed, 72 insertions(+) diff --git a/cel/env.go b/cel/env.go index 7139e415e..dac841402 100644 --- a/cel/env.go +++ b/cel/env.go @@ -1009,6 +1009,41 @@ func (i *Issues) Errors() []*Error { return i.errs.GetErrors() } +// Source returns the source associated with the issues. +func (i *Issues) Source() Source { + if i == nil { + return nil + } + return i.errs.Source() +} + +// ErrorOffset returns the start character offset for the error, if available. +func (i *Issues) ErrorOffset(err *Error) (int32, bool) { + offsetRange, found := i.ErrorOffsetRange(err) + if found { + return offsetRange.Start, true + } + return 0, false +} + +// ErrorOffsetRange returns the character offset range for the error, if available. +func (i *Issues) ErrorOffsetRange(err *Error) (celast.OffsetRange, bool) { + if i == nil || err == nil { + return celast.OffsetRange{}, false + } + if i.info != nil { + if offsetRange, found := i.info.GetOffsetRange(err.ExprID); found { + return offsetRange, true + } + } + if src := i.Source(); src != nil { + if offset, found := src.LocationOffset(err.Location); found { + return celast.OffsetRange{Start: offset, Stop: offset}, true + } + } + return celast.OffsetRange{}, false +} + // Append collects the issues from another Issues struct into a new Issues object. func (i *Issues) Append(other *Issues) *Issues { if i == nil { diff --git a/cel/env_test.go b/cel/env_test.go index 6dbc13696..0b3ca48cd 100644 --- a/cel/env_test.go +++ b/cel/env_test.go @@ -140,6 +140,38 @@ ERROR: :1:2: Syntax error: mismatched input '' expecting {'[', '{', } } +func TestIssuesErrorOffsetRange(t *testing.T) { + e, err := NewEnv() + if err != nil { + t.Fatalf("NewEnv() failed: %v", err) + } + _, iss := e.Compile("missing") + if len(iss.Errors()) != 1 { + t.Fatalf("iss.Errors() got %v, wanted 1 error", iss.Errors()) + } + errInfo := iss.Errors()[0] + + offset, found := iss.ErrorOffset(errInfo) + if !found { + t.Fatal("ErrorOffset() got found false, wanted true") + } + if offset != 0 { + t.Errorf("ErrorOffset() got %d, wanted 0", offset) + } + + offsetRange, found := iss.ErrorOffsetRange(errInfo) + if !found { + t.Fatal("ErrorOffsetRange() got found false, wanted true") + } + if offsetRange != (ast.OffsetRange{Start: 0, Stop: 7}) { + t.Errorf("ErrorOffsetRange() got %v, wanted [0, 7]", offsetRange) + } + + if iss.Source() == nil { + t.Fatal("Source() got nil, wanted source") + } +} + func TestFormatCELTypeEquivalence(t *testing.T) { values := []*Type{ AnyType, diff --git a/common/errors.go b/common/errors.go index c8865df8c..f39f75729 100644 --- a/common/errors.go +++ b/common/errors.go @@ -70,6 +70,11 @@ func (e *Errors) GetErrors() []*Error { return e.errors[:] } +// Source returns the source associated with the errors. +func (e *Errors) Source() Source { + return e.source +} + // Append creates a new Errors object with the current and input errors. func (e *Errors) Append(errs []*Error) *Errors { return &Errors{