build(deps): bump github.com/open-policy-agent/opa from 1.12.3 to 1.13.1

Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.12.3 to 1.13.1.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v1.12.3...v1.13.1)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-02-17 09:02:30 +01:00
committed by Ralf Haferkamp
parent 4e9eb596f0
commit 5daeada697
144 changed files with 18517 additions and 1969 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -111,6 +111,9 @@ opa_arith_rem,opa_bf_to_number
opa_array_concat,opa_value_type
opa_array_concat,opa_array_with_cap
opa_array_concat,opa_array_append
opa_array_flatten,opa_value_type
opa_array_flatten,opa_array_with_cap
opa_array_flatten,opa_array_append
opa_array_slice,opa_value_type
opa_array_slice,opa_number_try_int
opa_array_slice,opa_array_with_cap
1 opa_agg_count opa_value_type
111 opa_array_concat opa_value_type
112 opa_array_concat opa_array_with_cap
113 opa_array_concat opa_array_append
114 opa_array_flatten opa_value_type
115 opa_array_flatten opa_array_with_cap
116 opa_array_flatten opa_array_append
117 opa_array_slice opa_value_type
118 opa_array_slice opa_number_try_int
119 opa_array_slice opa_array_with_cap
Binary file not shown.
@@ -90,6 +90,7 @@ var builtinsFunctions = map[string]string{
ast.Floor.Name: "opa_arith_floor",
ast.Rem.Name: "opa_arith_rem",
ast.ArrayConcat.Name: "opa_array_concat",
ast.ArrayFlatten.Name: "opa_array_flatten",
ast.ArrayReverse.Name: "opa_array_reverse",
ast.ArraySlice.Name: "opa_array_slice",
ast.SetDiff.Name: "opa_set_diff",
+3 -3
View File
@@ -602,7 +602,7 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
return child.Unfold(path[1:])
}
return nil, fmt.Errorf("path %v does not exist in object term %v", ast.Ref{path[0]}, e.value.Value)
return nil, fmt.Errorf("path %v does not exist in object term %v", path[0], e.value.Value)
case ast.Set:
// Sets' keys *are* their values, so in order to allow accurate
// traversal, we have to collapse the tree beneath this node,
@@ -662,10 +662,10 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
return child.Unfold(path[1:])
}
return nil, fmt.Errorf("path %v does not exist in array term %v", ast.Ref{ast.IntNumberTerm(idx)}, e.value.Value)
return nil, fmt.Errorf("path %v does not exist in array term %v", ast.IntNumberTerm(idx), e.value.Value)
default:
// Catch all primitive types.
return nil, fmt.Errorf("expected composite type for path %v, found value: %v (type: %T)", ast.Ref{path[0]}, x, x)
return nil, fmt.Errorf("expected composite type for path %v, found value: %v (type: %T)", path[0], x, x)
}
}
@@ -2,11 +2,10 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package report provides functions to report OPA's version information to an external service and process the response.
package report
// Package versioncheck provides functions to check for the latest OPA release version from GitHub.
package versioncheck
import (
"cmp"
"context"
"encoding/json"
"errors"
@@ -14,7 +13,6 @@ import (
"net/http"
"os"
"runtime"
"strconv"
"strings"
"time"
@@ -34,24 +32,24 @@ import (
//
// Override at build time via:
//
// -ldflags "-X github.com/open-policy-agent/opa/internal/report.ExternalServiceURL=<url>"
// -ldflags "-X github.com/open-policy-agent/opa/internal/report.GHRepo=<url>"
// -ldflags "-X github.com/open-policy-agent/opa/internal/versioncheck.ExternalServiceURL=<url>"
// -ldflags "-X github.com/open-policy-agent/opa/internal/versioncheck.GHRepo=<url>"
//
// ExternalServiceURL will be overridden if the OPA_TELEMETRY_SERVICE_URL environment variable
// ExternalServiceURL will be overridden if the OPA_VERSION_CHECK_SERVICE_URL environment variable
// is provided.
var ExternalServiceURL = "https://api.github.com"
var GHRepo = "open-policy-agent/opa"
// Reporter reports information such as the version, heap usage about the running OPA instance to an external service
type Reporter interface {
SendReport(ctx context.Context) (*DataResponse, error)
// Checker checks for the latest OPA release version
type Checker interface {
LatestVersion(ctx context.Context) (*DataResponse, error)
RegisterGatherer(key string, f Gatherer)
}
// Gatherer represents a mechanism to inject additional data in the telemetry report
// Gatherer represents a mechanism to inject additional data (currently unused for version checking)
type Gatherer func(ctx context.Context) (any, error)
// DataResponse represents the data returned by the external service
// DataResponse represents the data returned by the version check
type DataResponse struct {
Latest ReleaseDetails `json:"latest"`
}
@@ -64,44 +62,48 @@ type ReleaseDetails struct {
OPAUpToDate bool `json:"opa_up_to_date,omitempty"` // is running OPA version greater than or equal to the latest released
}
// Options supplies parameters to the reporter.
// Options supplies parameters to the version checker.
type Options struct {
Logger logging.Logger
}
type GHVersionCollector struct {
type GitHubVersionChecker struct {
client rest.Client
}
type GHResponse struct {
type GitHubRelease struct {
TagName string `json:"tag_name,omitempty"` // latest OPA release tag
ReleaseNotes string `json:"html_url,omitempty"` // link to the OPA release notes
Download string `json:"assets_url,omitempty"` // link to download the OPA release
}
// New returns an instance of the Reporter
func New(opts Options) (Reporter, error) {
url := cmp.Or(os.Getenv("OPA_TELEMETRY_SERVICE_URL"), ExternalServiceURL)
// New returns an instance of the Checker
func New(opts Options) (Checker, error) {
url := os.Getenv("OPA_VERSION_CHECK_SERVICE_URL")
if url == "" {
url = ExternalServiceURL
}
// Set a generic User-Agent to avoid sending version/platform information about the user's OPA instance.
// This ensures we only retrieve version information without transmitting any identifying data.
restConfig := fmt.Appendf(nil, `{
"url": %q,
"headers": {
"User-Agent": "OPA-Version-Checker"
}
}`, url)
client, err := rest.New(restConfig, map[string]*keys.Config{}, rest.Logger(opts.Logger))
if err != nil {
return nil, err
}
r := GHVersionCollector{client: client}
// heap_usage_bytes is always present, so register it unconditionally
r.RegisterGatherer("heap_usage_bytes", readRuntimeMemStats)
r := GitHubVersionChecker{client: client}
return &r, nil
}
// SendReport sends the telemetry report which includes information such as the OPA version, current memory usage to
// the external service
func (r *GHVersionCollector) SendReport(ctx context.Context) (*DataResponse, error) {
// LatestVersion queries the GitHub API to check for the latest OPA release version
func (r *GitHubVersionChecker) LatestVersion(ctx context.Context) (*DataResponse, error) {
rCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
@@ -115,12 +117,12 @@ func (r *GHVersionCollector) SendReport(ctx context.Context) (*DataResponse, err
switch resp.StatusCode {
case http.StatusOK:
if resp.Body != nil {
var result GHResponse
var result GitHubRelease
err := json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return nil, err
}
return createDataResponse(result)
return createReleaseInfo(result)
}
return nil, nil
default:
@@ -128,7 +130,7 @@ func (r *GHVersionCollector) SendReport(ctx context.Context) (*DataResponse, err
}
}
func createDataResponse(ghResp GHResponse) (*DataResponse, error) {
func createReleaseInfo(ghResp GitHubRelease) (*DataResponse, error) {
if ghResp.TagName == "" {
return nil, errors.New("server response does not contain tag_name")
}
@@ -168,7 +170,7 @@ func createDataResponse(ghResp GHResponse) (*DataResponse, error) {
}, nil
}
func (*GHVersionCollector) RegisterGatherer(_ string, _ Gatherer) {
func (*GitHubVersionChecker) RegisterGatherer(_ string, _ Gatherer) {
// no-op for this implementation
}
@@ -206,9 +208,3 @@ func (dr *DataResponse) Pretty() string {
return strings.Join(lines, "\n")
}
func readRuntimeMemStats(_ context.Context) (any, error) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return strconv.FormatUint(m.Alloc, 10), nil
}
+8
View File
@@ -408,6 +408,14 @@ func Store(s storage.Store) func(r *Rego) {
return v1.Store(s)
}
// Data returns an argument that sets the Rego data document. Data should be
// a map representing the data document. This is a simpler alternative to
// using Store with inmem.NewFromObject for cases where an in-memory store
// with static data is sufficient.
func Data(x map[string]any) func(r *Rego) {
return v1.Data(x)
}
// StoreReadAST returns an argument that sets whether the store should eagerly convert data to AST values.
//
// Only applicable when no store has been set on the Rego object through the Store option.
+34 -20
View File
@@ -95,6 +95,7 @@ var DefaultBuiltins = [...]*Builtin{
// Arrays
ArrayConcat,
ArrayFlatten,
ArraySlice,
ArrayReverse,
@@ -893,6 +894,18 @@ var ArrayConcat = &Builtin{
CanSkipBctx: true,
}
var ArrayFlatten = &Builtin{
Name: "array.flatten",
Description: "Non-recursively unpacks array items in arr into the flattened array. Other types are appended as-is.",
Decl: types.NewFunction(
types.Args(
types.Named("arr", types.NewArray(nil, types.A)).Description("the array to be flattened"),
),
types.Named("flattened", types.NewArray(nil, types.A)).Description("array flattened one level"),
),
CanSkipBctx: true,
}
var ArraySlice = &Builtin{
Name: "array.slice",
Description: "Returns a slice of a given array. If `start` is greater or equal than `stop`, `slice` is `[]`.",
@@ -1823,7 +1836,8 @@ var ObjectKeys = &Builtin{
/*
* Encoding
*/
var encoding = category("encoding")
// Not using 'encoding' to avoid having to alias stdlib "encoding" imports
var catEncoding = category("encoding")
var JSONMarshal = &Builtin{
Name: "json.marshal",
@@ -1834,7 +1848,7 @@ var JSONMarshal = &Builtin{
),
types.Named("y", types.S).Description("the JSON string representation of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1856,7 +1870,7 @@ var JSONMarshalWithOptions = &Builtin{
),
types.Named("y", types.S).Description("the JSON string representation of `x`, with configured prefix/indent string(s) as appropriate"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1869,7 +1883,7 @@ var JSONUnmarshal = &Builtin{
),
types.Named("y", types.A).Description("the term deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1882,7 +1896,7 @@ var JSONIsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid JSON, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1895,7 +1909,7 @@ var Base64Encode = &Builtin{
),
types.Named("y", types.S).Description("base64 serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1908,7 +1922,7 @@ var Base64Decode = &Builtin{
),
types.Named("y", types.S).Description("base64 deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1921,7 +1935,7 @@ var Base64IsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid base64 encoded value, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1934,7 +1948,7 @@ var Base64UrlEncode = &Builtin{
),
types.Named("y", types.S).Description("base64url serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1947,7 +1961,7 @@ var Base64UrlEncodeNoPad = &Builtin{
),
types.Named("y", types.S).Description("base64url serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1960,7 +1974,7 @@ var Base64UrlDecode = &Builtin{
),
types.Named("y", types.S).Description("base64url deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1973,7 +1987,7 @@ var URLQueryDecode = &Builtin{
),
types.Named("y", types.S).Description("URL-encoding deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1986,7 +2000,7 @@ var URLQueryEncode = &Builtin{
),
types.Named("y", types.S).Description("URL-encoding serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2010,7 +2024,7 @@ var URLQueryEncodeObject = &Builtin{
),
types.Named("y", types.S).Description("the URL-encoded serialization of `object`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2025,7 +2039,7 @@ var URLQueryDecodeObject = &Builtin{
types.S,
types.NewArray(nil, types.S)))).Description("the resulting object"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2038,7 +2052,7 @@ var YAMLMarshal = &Builtin{
),
types.Named("y", types.S).Description("the YAML string representation of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2051,7 +2065,7 @@ var YAMLUnmarshal = &Builtin{
),
types.Named("y", types.A).Description("the term deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2065,7 +2079,7 @@ var YAMLIsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid YAML, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2078,7 +2092,7 @@ var HexEncode = &Builtin{
),
types.Named("y", types.S).Description("serialization of `x` using hex-encoding"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2091,7 +2105,7 @@ var HexDecode = &Builtin{
),
types.Named("y", types.S).Description("deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
+22 -5
View File
@@ -6,6 +6,7 @@ package ast
import (
"fmt"
"regexp"
"slices"
"strings"
@@ -387,10 +388,12 @@ func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error {
return NewError(TypeErr, expr.Location, "undefined function %v", name)
}
// check if the expression refers to a function that contains an error
_, ok := tpe.(types.Any)
if ok {
return nil
if t, ok := tpe.(types.Any); ok {
// A type.Any with a len(0) is created by using types.A , this represents a potential non-local reference
// This is the exception when checking if the type represents a function
if len(t) == 0 {
return nil
}
}
ftpe, ok := tpe.(*types.Function)
@@ -1087,7 +1090,21 @@ func newRefErrInvalid(loc *Location, ref Ref, idx int, have, want types.Type, on
}
func newRefErrUnsupported(loc *Location, ref Ref, idx int, have types.Type) *Error {
err := newRefError(loc, ref)
var err *Error
switch have.(type) {
case *types.Function:
var function string
// drop any trailing references to unidentified parameters (e.g. __local1__)
if match, err := regexp.MatchString(`__local[0-9]+__`, ref[len(ref)-1].Value.String()); err == nil && match {
function = ref[:len(ref)-1].String()
} else {
function = ref.String()
}
err = NewError(TypeErr, loc, "function %s used as reference, not called", function)
default:
err = newRefError(loc, ref)
}
err.Details = &RefErrUnsupportedDetail{
Ref: ref,
Pos: idx,
+2
View File
@@ -162,6 +162,8 @@ func (env *TypeEnv) getRefFallback(ref Ref) types.Type {
}
if RootDocumentNames.Contains(ref[0]) {
// types.A is an empty types.Any
// this is used to represent a potential non-local reference
return types.A
}
+32 -9
View File
@@ -8,6 +8,7 @@ import (
"fmt"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
// Location records a position in source code
@@ -28,10 +29,10 @@ func NewLocation(text []byte, file string, row int, col int) *Location {
// Equal checks if two locations are equal to each other.
func (loc *Location) Equal(other *Location) bool {
return bytes.Equal(loc.Text, other.Text) &&
loc.File == other.File &&
return loc.File == other.File &&
loc.Row == other.Row &&
loc.Col == other.Col
loc.Col == other.Col &&
bytes.Equal(loc.Text, other.Text)
}
// Errorf returns a new error value with a message formatted to include the location
@@ -57,13 +58,35 @@ func (loc *Location) Format(f string, a ...any) string {
}
func (loc *Location) String() string {
if len(loc.File) > 0 {
return fmt.Sprintf("%v:%v", loc.File, loc.Row)
buf, _ := loc.AppendText(make([]byte, 0, loc.StringLength()))
return util.ByteSliceToString(buf)
}
func (loc *Location) AppendText(buf []byte) ([]byte, error) {
if loc != nil {
switch {
case len(loc.File) > 0:
buf = util.AppendInt(append(append(buf, loc.File...), ':'), loc.Row)
case len(loc.Text) > 0:
buf = append(buf, loc.Text...)
default:
buf = util.AppendInt(append(util.AppendInt(buf, loc.Row), ':'), loc.Col)
}
}
if len(loc.Text) > 0 {
return string(loc.Text)
return buf, nil
}
func (loc *Location) StringLength() (n int) {
if loc != nil {
if l := len(loc.File); l > 0 {
n = l + 1 + util.NumDigitsInt(loc.Row)
} else if l := len(loc.Text); l > 0 {
n = l
} else {
n = util.NumDigitsInt(loc.Row) + 1 + util.NumDigitsInt(loc.Col)
}
}
return fmt.Sprintf("%v:%v", loc.Row, loc.Col)
return n
}
// Compare returns -1, 0, or 1 to indicate if this loc is less than, equal to,
@@ -71,7 +94,7 @@ func (loc *Location) String() string {
// column of the Location (but not on the text.) Nil locations are greater than
// non-nil locations.
func (loc *Location) Compare(other *Location) int {
if loc == nil && other == nil {
if loc == other {
return 0
} else if loc == nil {
return 1
+91 -83
View File
@@ -20,7 +20,7 @@ import (
"strings"
"unicode/utf8"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/open-policy-agent/opa/v1/ast/internal/scanner"
"github.com/open-policy-agent/opa/v1/ast/internal/tokens"
@@ -71,6 +71,10 @@ var (
// copy them to the call term only when needed
memberWithKeyRef = MemberWithKey.Ref()
memberRef = Member.Ref()
newlineBytes = []byte{'\n'}
metadataBytes = []byte("METADATA")
metadataParserPool = util.NewSyncPool[metadataParser]()
)
func (v RegoVersion) Int() int {
@@ -540,44 +544,46 @@ func (p *Parser) parseAnnotations(stmts []Statement) []Statement {
return stmts
}
func parseAnnotations(comments []*Comment) ([]*Annotations, Errors) {
func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) {
numBlocks := CountFunc(comments, isMetadataComment)
if numBlocks == 0 {
return nil, nil
}
var hint = []byte("METADATA")
var curr *metadataParser
var blocks []*metadataParser
stmts = make([]*Annotations, 0, numBlocks)
mdp := metadataParserPool.Get()
if mdp.buf == nil {
mdp.buf = &bytes.Buffer{}
}
for i := range comments {
if curr != nil {
if comments[i].Location.Row == comments[i-1].Location.Row+1 && comments[i].Location.Col == 1 {
curr.Append(comments[i])
continue
if isMetadataComment(comments[i]) { // scan until end of block
mdp.Reset(comments[i].Location)
for i++; i < len(comments) && !blockBuster(comments[i], comments[i-1]); i++ {
mdp.Append(comments[i])
}
if a, err := mdp.Parse(); err != nil {
errs = append(errs, &Error{Code: ParseErr, Message: err.Error(), Location: mdp.loc})
} else {
stmts = append(stmts, a)
}
curr = nil
}
if bytes.HasPrefix(bytes.TrimSpace(comments[i].Text), hint) {
curr = newMetadataParser(comments[i].Location)
blocks = append(blocks, curr)
}
}
stmts := make([]*Annotations, 0, len(blocks))
var errs Errors
for _, b := range blocks {
if a, err := b.Parse(); err != nil {
errs = append(errs, &Error{
Code: ParseErr,
Message: err.Error(),
Location: b.loc,
})
} else {
stmts = append(stmts, a)
}
}
metadataParserPool.Put(mdp)
return stmts, errs
}
func isMetadataComment(c *Comment) bool {
return c.Location.Col == 1 && bytes.HasPrefix(bytes.TrimSpace(c.Text), metadataBytes)
}
func blockBuster(curr, prev *Comment) bool { // or endOfBlock, but the name was too good to pass up
return curr.Location.Col != 1 || curr.Location.Row-1 != prev.Location.Row
}
func (p *Parser) parsePackage() *Package {
if p.s.tok != tokens.Package {
return nil
@@ -2455,7 +2461,8 @@ func (p *Parser) parseTermPairList(end tokens.Token, r [][2]*Term) [][2]*Term {
func (p *Parser) parseTermOp(values ...tokens.Token) *Term {
if slices.Contains(values, p.s.tok) {
r := RefTerm(VarTerm(p.s.tok.String()).SetLocation(p.s.Loc())).SetLocation(p.s.Loc())
loc := p.s.Loc()
r := RefTerm(VarTerm(p.s.tok.String()).SetLocation(loc)).SetLocation(loc)
p.scan()
return r
}
@@ -2465,11 +2472,12 @@ func (p *Parser) parseTermOp(values ...tokens.Token) *Term {
func (p *Parser) parseTermOpName(ref Ref, values ...tokens.Token) *Term {
if slices.Contains(values, p.s.tok) {
cp := ref.Copy()
loc := p.s.Loc()
for _, r := range cp {
r.SetLocation(p.s.Loc())
r.SetLocation(loc)
}
t := RefTerm(cp...)
t.SetLocation(p.s.Loc())
t.SetLocation(loc)
p.scan()
return t
}
@@ -2743,13 +2751,17 @@ type rawAnnotation struct {
}
type metadataParser struct {
buf *bytes.Buffer
comments []*Comment
buf *bytes.Buffer
loc *location.Location
}
func newMetadataParser(loc *Location) *metadataParser {
return &metadataParser{loc: loc, buf: bytes.NewBuffer(nil)}
func (b *metadataParser) Reset(loc *location.Location) {
b.comments = b.comments[:0]
b.loc = loc
if b.buf != nil {
b.buf.Reset()
}
}
func (b *metadataParser) Append(c *Comment) {
@@ -2760,14 +2772,12 @@ func (b *metadataParser) Append(c *Comment) {
var yamlLineErrRegex = regexp.MustCompile(`^yaml:(?: unmarshal errors:[\n\s]*)? line ([[:digit:]]+):`)
func (b *metadataParser) Parse() (*Annotations, error) {
var raw rawAnnotation
func (b *metadataParser) Parse() (result *Annotations, err error) {
if len(bytes.TrimSpace(b.buf.Bytes())) == 0 {
return nil, errors.New("expected METADATA block, found whitespace")
}
var raw rawAnnotation
if err := yaml.Unmarshal(b.buf.Bytes(), &raw); err != nil {
var comment *Comment
match := yamlLineErrRegex.FindStringSubmatch(err.Error())
@@ -2790,13 +2800,14 @@ func (b *metadataParser) Parse() (*Annotations, error) {
return nil, augmentYamlError(err, b.comments)
}
var result Annotations
result.comments = b.comments
result.Scope = raw.Scope
result.Entrypoint = raw.Entrypoint
result.Title = raw.Title
result.Description = raw.Description
result.Organizations = raw.Organizations
result = &Annotations{
comments: b.comments,
Scope: raw.Scope,
Entrypoint: raw.Entrypoint,
Title: raw.Title,
Description: raw.Description,
Organizations: raw.Organizations,
}
for _, v := range raw.RelatedResources {
rr, err := parseRelatedResource(v)
@@ -2878,32 +2889,30 @@ func (b *metadataParser) Parse() (*Annotations, error) {
result.Authors = append(result.Authors, author)
}
result.Custom = make(map[string]any)
for k, v := range raw.Custom {
val, err := convertYAMLMapKeyTypes(v, nil)
if err != nil {
return nil, err
if raw.Custom != nil {
result.Custom = make(map[string]any, len(raw.Custom))
for k, v := range raw.Custom {
if result.Custom[k], err = convertYAMLMapKeyTypes(v, nil); err != nil {
return nil, err
}
}
result.Custom[k] = val
}
result.Location = b.loc
// recreate original text of entire metadata block for location text attribute
sb := strings.Builder{}
sb.WriteString("# METADATA\n")
original := bytes.TrimSuffix(b.buf.Bytes(), newlineBytes)
numLines := bytes.Count(original, newlineBytes) + 1
preAlloc := len("# METADATA\n") + len(original) + numLines*2 // '# ' prefix added per line
lines := bytes.Split(b.buf.Bytes(), []byte{'\n'})
result.Location.Text = append(make([]byte, 0, preAlloc), "# METADATA\n"...)
for _, line := range lines[:len(lines)-1] {
sb.WriteString("# ")
sb.Write(line)
sb.WriteByte('\n')
for line := range bytes.SplitAfterSeq(original, newlineBytes) {
result.Location.Text = append(result.Location.Text, "# "...)
result.Location.Text = append(result.Location.Text, line...)
}
result.Location.Text = []byte(strings.TrimSuffix(sb.String(), "\n"))
return &result, nil
return result, err
}
// augmentYamlError augments a YAML error with hints intended to help the user figure out the cause of an otherwise
@@ -2912,30 +2921,29 @@ func (b *metadataParser) Parse() (*Annotations, error) {
func augmentYamlError(err error, comments []*Comment) error {
// Adding hints for when key/value ':' separator isn't suffixed with a legal YAML space symbol
for _, comment := range comments {
txt := string(comment.Text)
parts := strings.Split(txt, ":")
if len(parts) > 1 {
parts = parts[1:]
var invalidSpaces []string
for partIndex, part := range parts {
if len(part) == 0 && partIndex == len(parts)-1 {
invalidSpaces = []string{}
break
}
if bytes.IndexByte(comment.Text, ':') == -1 {
continue
}
parts := bytes.Split(comment.Text, []byte{':'})[1:]
r, _ := utf8.DecodeRuneInString(part)
if r == ' ' || r == '\t' {
invalidSpaces = []string{}
break
}
var invalidSpaces []string
for partIndex, part := range parts {
if len(part) == 0 && partIndex == len(parts)-1 {
break
}
invalidSpaces = append(invalidSpaces, fmt.Sprintf("%+q", r))
}
if len(invalidSpaces) > 0 {
err = fmt.Errorf(
"%s\n Hint: on line %d, symbol(s) %v immediately following a key/value separator ':' is not a legal yaml space character",
err.Error(), comment.Location.Row, invalidSpaces)
r, _ := utf8.DecodeRune(part)
if r == ' ' || r == '\t' {
break
}
invalidSpaces = append(invalidSpaces, fmt.Sprintf("%+q", r))
}
if len(invalidSpaces) > 0 {
err = fmt.Errorf(
"%s\n Hint: on line %d, symbol(s) %v immediately following a"+
" key/value separator ':' is not a legal yaml space character",
err.Error(), comment.Location.Row, invalidSpaces)
}
}
return err
@@ -3053,7 +3061,7 @@ func parseAuthorString(s string) (*AuthorAnnotation, error) {
if len(trailing) >= len(emailPrefix)+len(emailSuffix) && strings.HasPrefix(trailing, emailPrefix) &&
strings.HasSuffix(trailing, emailSuffix) {
email = trailing[len(emailPrefix):]
email = email[0 : len(email)-len(emailSuffix)]
email = email[:len(email)-len(emailSuffix)]
namePartCount -= 1
}
+14
View File
@@ -4,6 +4,7 @@
package ast
import (
"encoding"
"strings"
"sync"
)
@@ -83,3 +84,16 @@ func BuiltinNameFromRef(ref Ref) (string, bool) {
return "", false
}
func AppendDelimeted[T encoding.TextAppender](buf []byte, appenders []T, delim string) ([]byte, error) {
for i, item := range appenders {
if i > 0 {
buf = append(buf, delim...)
}
var err error
if buf, err = item.AppendText(buf); err != nil {
return nil, err
}
}
return buf, nil
}
+75 -194
View File
@@ -371,42 +371,8 @@ func (mod *Module) Equal(other *Module) bool {
}
func (mod *Module) String() string {
byNode := map[Node][]*Annotations{}
for _, a := range mod.Annotations {
byNode[a.node] = append(byNode[a.node], a)
}
appendAnnotationStrings := func(buf []string, node Node) []string {
if as, ok := byNode[node]; ok {
for i := range as {
buf = append(buf,
"# METADATA",
"# "+as[i].String(),
)
}
}
return buf
}
buf := []string{}
buf = appendAnnotationStrings(buf, mod.Package)
buf = append(buf, mod.Package.String())
if len(mod.Imports) > 0 {
buf = append(buf, "")
for _, imp := range mod.Imports {
buf = appendAnnotationStrings(buf, imp)
buf = append(buf, imp.String())
}
}
if len(mod.Rules) > 0 {
buf = append(buf, "")
for _, rule := range mod.Rules {
buf = appendAnnotationStrings(buf, rule)
buf = append(buf, rule.stringWithOpts(toStringOpts{regoVersion: mod.regoVersion}))
}
}
return strings.Join(buf, "\n")
buf, _ := mod.AppendText(make([]byte, 0, mod.StringLength()))
return util.ByteSliceToString(buf)
}
// RuleSet returns a RuleSet containing named rules in the mod.
@@ -475,7 +441,8 @@ func (c *Comment) SetLoc(loc *Location) {
}
func (c *Comment) String() string {
return "#" + string(c.Text)
buf, _ := c.AppendText(make([]byte, 0, c.StringLength()))
return util.ByteSliceToString(buf)
}
// Copy returns a deep copy of c.
@@ -525,16 +492,8 @@ func (pkg *Package) SetLoc(loc *Location) {
}
func (pkg *Package) String() string {
if pkg == nil {
return "<illegal nil package>"
} else if len(pkg.Path) <= 1 {
return fmt.Sprintf("package <illegal path %q>", pkg.Path)
}
// Omit head as all packages have the DefaultRootDocument prepended at parse time.
path := make(Ref, len(pkg.Path)-1)
path[0] = VarTerm(string(pkg.Path[1].Value.(String)))
copy(path[1:], pkg.Path[2:])
return fmt.Sprintf("package %v", path)
buf, _ := pkg.AppendText(make([]byte, 0, pkg.StringLength()))
return util.ByteSliceToString(buf)
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
@@ -637,11 +596,8 @@ func (imp *Import) Name() Var {
}
func (imp *Import) String() string {
buf := []string{"import", imp.Path.String()}
if len(imp.Alias) > 0 {
buf = append(buf, "as", imp.Alias.String())
}
return strings.Join(buf, " ")
buf, _ := imp.AppendText(make([]byte, 0, imp.StringLength()))
return util.ByteSliceToString(buf)
}
func (imp *Import) MarshalJSON() ([]byte, error) {
@@ -752,11 +708,12 @@ func (rule *Rule) Ref() Ref {
}
func (rule *Rule) String() string {
regoVersion := DefaultRegoVersion
opts := toStringOpts{}
if rule.Module != nil {
regoVersion = rule.Module.RegoVersion()
opts.regoVersion = rule.Module.RegoVersion()
}
return rule.stringWithOpts(toStringOpts{regoVersion: regoVersion})
buf, _ := rule.appendWithOpts(opts, make([]byte, 0, rule.stringLengthWithOpts(opts)))
return util.ByteSliceToString(buf)
}
type toStringOpts struct {
@@ -770,80 +727,46 @@ func (o toStringOpts) RegoVersion() RegoVersion {
return o.regoVersion
}
func (rule *Rule) stringWithOpts(opts toStringOpts) string {
buf := []string{}
if rule.Default {
buf = append(buf, "default")
}
buf = append(buf, rule.Head.stringWithOpts(opts))
if !rule.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, "if")
}
buf = append(buf, "{", rule.Body.String(), "}")
}
if rule.Else != nil {
buf = append(buf, rule.Else.elseString(opts))
}
return strings.Join(buf, " ")
}
func (rule *Rule) isFunction() bool {
return len(rule.Head.Args) > 0
}
// ruleJSON is used for JSON serialization of Rule to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type ruleJSON struct {
Annotations []*Annotations `json:"annotations,omitempty"`
Body Body `json:"body"`
Default bool `json:"default,omitempty"`
Else *Rule `json:"else,omitempty"`
Head *Head `json:"head"`
Location *Location `json:"location,omitempty"`
}
func (rule *Rule) MarshalJSON() ([]byte, error) {
data := map[string]any{
"head": rule.Head,
"body": rule.Body,
data := ruleJSON{
Head: rule.Head,
Body: rule.Body,
}
if rule.Default {
data["default"] = true
data.Default = true
}
if rule.Else != nil {
data["else"] = rule.Else
data.Else = rule.Else
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
if rule.Location != nil {
data["location"] = rule.Location
}
data.Location = rule.Location
}
if len(rule.Annotations) != 0 {
data["annotations"] = rule.Annotations
data.Annotations = rule.Annotations
}
return json.Marshal(data)
}
func (rule *Rule) elseString(opts toStringOpts) string {
var buf []string
buf = append(buf, "else")
value := rule.Head.Value
if value != nil {
buf = append(buf, "=", value.String())
}
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, "if")
}
buf = append(buf, "{", rule.Body.String(), "}")
if rule.Else != nil {
buf = append(buf, rule.Else.elseString(opts))
}
return strings.Join(buf, " ")
}
// NewHead returns a new Head object. If args are provided, the first will be
// used for the key and the second will be used for the value.
func NewHead(name Var, args ...*Term) *Head {
@@ -1002,37 +925,8 @@ func (head *Head) String() string {
}
func (head *Head) stringWithOpts(opts toStringOpts) string {
buf := strings.Builder{}
buf.WriteString(head.Ref().String())
containsAdded := false
switch {
case len(head.Args) != 0:
buf.WriteString(head.Args.String())
case len(head.Reference) == 1 && head.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
buf.WriteRune('[')
buf.WriteString(head.Key.String())
buf.WriteRune(']')
default:
containsAdded = true
buf.WriteString(" contains ")
buf.WriteString(head.Key.String())
}
}
if head.Value != nil {
if head.Assign {
buf.WriteString(" := ")
} else {
buf.WriteString(" = ")
}
buf.WriteString(head.Value.String())
} else if !containsAdded && head.Name == "" && head.Key != nil {
buf.WriteString(" contains ")
buf.WriteString(head.Key.String())
}
return buf.String()
buf, _ := head.appendWithOpts(opts, make([]byte, 0, head.stringLengthWithOpts(opts)))
return util.ByteSliceToString(buf)
}
func (head *Head) MarshalJSON() ([]byte, error) {
@@ -1103,11 +997,8 @@ func (a Args) Copy() Args {
}
func (a Args) String() string {
buf := make([]string, 0, len(a))
for _, t := range a {
buf = append(buf, t.String())
}
return "(" + strings.Join(buf, ", ") + ")"
buf, _ := a.AppendText(make([]byte, 0, a.StringLength()))
return util.ByteSliceToString(buf)
}
// Loc returns the Location of a.
@@ -1240,11 +1131,12 @@ func (body Body) SetLoc(loc *Location) {
}
func (body Body) String() string {
buf := make([]string, 0, len(body))
for _, v := range body {
buf = append(buf, v.String())
}
return strings.Join(buf, "; ")
buf, _ := body.AppendText(make([]byte, 0, body.StringLength()))
return util.ByteSliceToString(buf)
}
func (body Body) AppendText(buf []byte) ([]byte, error) {
return AppendDelimeted(buf, body, "; ")
}
// Vars returns a VarSet containing variables in body. The params can be set to
@@ -1555,50 +1447,41 @@ func (expr *Expr) SetLoc(loc *Location) {
}
func (expr *Expr) String() string {
buf := make([]string, 0, 2+len(expr.With))
if expr.Negated {
buf = append(buf, "not")
}
switch t := expr.Terms.(type) {
case []*Term:
if expr.IsEquality() && validEqAssignArgCount(expr) {
buf = append(buf, fmt.Sprintf("%v %v %v", t[1], Equality.Infix, t[2]))
} else {
buf = append(buf, Call(t).String())
}
case fmt.Stringer:
buf = append(buf, t.String())
}
buf, _ := expr.AppendText(make([]byte, 0, expr.StringLength()))
return util.ByteSliceToString(buf)
}
for i := range expr.With {
buf = append(buf, expr.With[i].String())
}
return strings.Join(buf, " ")
// exprJSON is used for JSON serialization of Expr to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type exprJSON struct {
Generated bool `json:"generated,omitempty"`
Index int `json:"index"`
Location *Location `json:"location,omitempty"`
Negated bool `json:"negated,omitempty"`
Terms any `json:"terms"`
With []*With `json:"with,omitempty"`
}
func (expr *Expr) MarshalJSON() ([]byte, error) {
data := map[string]any{
"terms": expr.Terms,
"index": expr.Index,
data := exprJSON{
Index: expr.Index,
Terms: expr.Terms,
}
if len(expr.With) > 0 {
data["with"] = expr.With
data.With = expr.With
}
if expr.Generated {
data["generated"] = true
data.Generated = true
}
if expr.Negated {
data["negated"] = true
data.Negated = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
if expr.Location != nil {
data["location"] = expr.Location
}
data.Location = expr.Location
}
return json.Marshal(data)
@@ -1668,17 +1551,8 @@ func visitCogeneratedExprs(expr *Expr, f func(*Expr) bool) {
}
func (d *SomeDecl) String() string {
if call, ok := d.Symbols[0].Value.(Call); ok {
if len(call) == 4 {
return "some " + call[1].String() + ", " + call[2].String() + " in " + call[3].String()
}
return "some " + call[1].String() + " in " + call[2].String()
}
buf := make([]string, len(d.Symbols))
for i := range buf {
buf[i] = d.Symbols[i].String()
}
return "some " + strings.Join(buf, ", ")
buf, _ := d.AppendText(make([]byte, 0, d.StringLength()))
return util.ByteSliceToString(buf)
}
// SetLoc sets the Location on d.
@@ -1797,7 +1671,8 @@ func (q *Every) MarshalJSON() ([]byte, error) {
}
func (w *With) String() string {
return "with " + w.Target.String() + " as " + w.Value.String()
buf, _ := w.AppendText(make([]byte, 0, w.StringLength()))
return util.ByteSliceToString(buf)
}
// Equal returns true if this With is equals the other With.
@@ -1854,16 +1729,22 @@ func (w *With) SetLoc(loc *Location) {
w.Location = loc
}
// withJSON is used for JSON serialization of With to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type withJSON struct {
Location *Location `json:"location,omitempty"`
Target *Term `json:"target"`
Value *Term `json:"value"`
}
func (w *With) MarshalJSON() ([]byte, error) {
data := map[string]any{
"target": w.Target,
"value": w.Value,
data := withJSON{
Target: w.Target,
Value: w.Value,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
if w.Location != nil {
data["location"] = w.Location
}
data.Location = w.Location
}
return json.Marshal(data)
+333
View File
@@ -0,0 +1,333 @@
package ast
import (
"encoding"
"fmt"
)
func (m *Module) AppendText(buf []byte) ([]byte, error) {
if m == nil {
return append(buf, "<nil module>"...), nil
}
var err error
// NOTE(anderseknert): this DOES allocate still, and while that's unfortunate,
// we'll be better off dealing with that when we have v2 JSON in the stdlib than
// doing manual JSON marshalling (and string length calculations) here.
for _, annotations := range m.Annotations {
// rule annotations are attached to rules, so only check for package scoped ones here
if annotations.Scope == "package" || annotations.Scope == "subpackages" {
buf = append(buf, "# METADATA\n# "...)
buf = append(buf, annotations.String()...)
buf = append(buf, '\n')
}
}
if buf, err = m.Package.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, '\n')
if len(m.Imports) > 0 {
for _, imp := range m.Imports {
buf = append(buf, '\n')
if buf, err = imp.AppendText(buf); err != nil {
return nil, err
}
}
buf = append(buf, '\n')
}
if len(m.Rules) > 0 {
for _, rule := range m.Rules {
buf = append(buf, '\n')
if buf, err = rule.appendWithOpts(toStringOpts{regoVersion: m.regoVersion}, buf); err != nil {
return nil, err
}
}
}
return buf, nil
}
func (pkg *Package) AppendText(buf []byte) ([]byte, error) {
var err error
if pkg == nil {
return append(buf, "<illegal nil package>"...), nil
}
if len(pkg.Path) <= 1 {
buf = append(buf, "package <illegal path \""...)
if buf, err = pkg.Path.AppendText(buf); err != nil {
return nil, err
}
return append(buf, "\">"...), nil
}
buf = append(buf, "package "...)
path := pkg.Path[1:] // omit "data"
if s, ok := path[0].Value.(String); ok {
buf = append(buf, s...) // first term should never be quoted
if len(path) == 1 {
return buf, nil
}
buf = append(buf, '.')
path = path[1:]
}
return path.AppendText(buf)
}
func (imp *Import) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "import "...)
var err error
if buf, err = imp.Path.AppendText(buf); err != nil {
return nil, err
}
if imp.Alias != "" {
buf = append(buf, ' ', 'a', 's', ' ')
buf = append(buf, imp.Alias...)
}
return buf, nil
}
func (r *Rule) AppendText(buf []byte) ([]byte, error) {
regoVersion := DefaultRegoVersion
if r.Module != nil {
regoVersion = r.Module.RegoVersion()
}
return r.appendWithOpts(toStringOpts{regoVersion: regoVersion}, buf)
}
func (r *Rule) appendWithOpts(opts toStringOpts, buf []byte) ([]byte, error) {
// See note in [Module.AppendText] regarding annotations.
for _, annotations := range r.Annotations {
buf = append(buf, "# METADATA\n# "...)
buf = append(buf, annotations.String()...)
buf = append(buf, '\n')
}
if r.Default {
buf = append(buf, "default "...)
}
var err error
if buf, err = r.Head.appendWithOpts(opts, buf); err != nil {
return nil, err
}
if !r.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, " if { "...)
default:
buf = append(buf, " { "...)
}
if buf, err = r.Body.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " }"...)
}
if r.Else != nil {
if buf, err = r.Else.appendElse(opts, buf); err != nil {
return nil, err
}
}
return buf, nil
}
func (r *Rule) appendElse(opts toStringOpts, buf []byte) ([]byte, error) {
buf = append(buf, " else "...)
var err error
if r.Head.Value != nil {
buf = append(buf, "= "...)
if buf, err = r.Head.Value.AppendText(buf); err != nil {
return nil, err
}
}
if v := opts.RegoVersion(); v == RegoV1 || v == RegoV0CompatV1 {
buf = append(buf, " if { "...)
} else {
buf = append(buf, " { "...)
}
if buf, err = r.Body.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " }"...)
if r.Else != nil {
if buf, err = r.Else.appendElse(opts, buf); err != nil {
return nil, err
}
}
return buf, nil
}
func (h *Head) AppendText(buf []byte) ([]byte, error) {
return h.appendWithOpts(toStringOpts{}, buf)
}
func (h *Head) appendWithOpts(opts toStringOpts, buf []byte) ([]byte, error) {
var err error
if h.Reference == nil {
buf = append(buf, h.Name...)
} else {
if buf, err = h.Reference.AppendText(buf); err != nil {
return nil, err
}
}
containsAdded := false
switch {
case len(h.Args) != 0:
if buf, err = h.Args.AppendText(buf); err != nil {
return nil, err
}
case len(h.Reference) == 1 && h.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
buf = append(buf, '[')
if buf, err = h.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ']')
default:
if buf, err = h.Key.AppendText(append(buf, " contains "...)); err != nil {
return nil, err
}
containsAdded = true
}
}
if h.Value != nil {
if h.Assign {
buf = append(buf, " := "...)
} else {
buf = append(buf, " = "...)
}
if buf, err = h.Value.AppendText(buf); err != nil {
return nil, err
}
} else if !containsAdded && h.Name == "" && h.Key != nil {
if buf, err = h.Key.AppendText(append(buf, " contains "...)); err != nil {
return nil, err
}
}
return buf, nil
}
func (a Args) AppendText(buf []byte) ([]byte, error) {
var err error
buf = append(buf, '(')
if buf, err = AppendDelimeted(buf, a, ", "); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
func (expr *Expr) AppendText(buf []byte) ([]byte, error) {
if expr.Negated {
buf = append(buf, "not "...)
}
var err error
switch t := expr.Terms.(type) {
case []*Term:
if expr.IsEquality() && validEqAssignArgCount(expr) {
if buf, err = t[1].AppendText(buf); err != nil {
return nil, err
}
buf = append(append(append(buf, ' '), Equality.Infix...), ' ')
if buf, err = t[2].AppendText(buf); err != nil {
return nil, err
}
} else if buf, err = Call(t).AppendText(buf); err != nil {
return nil, err
}
case encoding.TextAppender:
if buf, err = t.AppendText(buf); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported expr terms type: %T", expr.Terms)
}
if len(expr.With) > 0 {
buf = append(buf, ' ')
}
return AppendDelimeted(buf, expr.With, " ")
}
func (w *With) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "with "...)
var err error
if buf, err = w.Target.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " as "...)
if buf, err = w.Value.AppendText(buf); err != nil {
return nil, err
}
return buf, nil
}
func (w *Every) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "every "...)
var err error
if w.Key != nil {
if buf, err = w.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ", "...)
}
if buf, err = w.Value.AppendText(buf); err == nil {
buf = append(buf, " in "...)
if buf, err = w.Domain.AppendText(buf); err == nil {
buf = append(buf, " { "...)
if buf, err = w.Body.AppendText(buf); err == nil {
buf = append(buf, " }"...)
}
}
}
return buf, err
}
func (d *SomeDecl) AppendText(buf []byte) ([]byte, error) {
var err error
buf = append(buf, "some "...)
if call, ok := d.Symbols[0].Value.(Call); ok {
if buf, err = call[1].AppendText(buf); err != nil {
return nil, err
}
if len(call) == 3 {
buf = append(buf, " in "...)
} else {
buf = append(buf, ", "...)
}
if buf, err = call[2].AppendText(buf); err != nil {
return nil, err
}
if len(call) == 4 {
buf = append(buf, " in "...)
if buf, err = call[3].AppendText(buf); err != nil {
return nil, err
}
}
return buf, nil
}
buf, err = AppendDelimeted(buf, d.Symbols, ", ")
return buf, err
}
func (c *Comment) AppendText(buf []byte) ([]byte, error) {
return append(append(buf, '#'), c.Text...), nil
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package ast
// CountFunc counts the number of items in a slice S that satisfy predicate function f.
func CountFunc[T any, S ~[]T](items S, f func(T) bool) (n int) {
for i := range items {
if f(items[i]) {
n++
}
}
return n
}
+347
View File
@@ -0,0 +1,347 @@
package ast
import (
"fmt"
"unicode/utf8"
"github.com/open-policy-agent/opa/v1/util"
)
// StringLengther is an interface for types that can report their string length without
// actually constructing the string. This is useful for pre-allocating buffers, like those
// used in AppendText, strings.Builder, bytes.Buffer, etc.
type StringLengther interface {
StringLength() int
}
// TermSliceStringLength returns the total string length of the given terms, as reported
// by the [StringLengther.StringLength] method implementation of each term's [Value]. The
// delimLen value will be added between each term's length to account for a delimiter, or
// no delimiter if delimLen is 0.
// Implementation note: this function is optimized for inlining, and just meets the threshold
// for that. Don't change without making sure that's still the case.
func TermSliceStringLength(terms []*Term, delimLen int) (n int) {
for i := range terms {
n += terms[i].StringLength() + delimLen
}
return max(n-delimLen, 0)
}
func (t *Term) StringLength() int {
if sl, ok := t.Value.(StringLengther); ok {
return sl.StringLength()
}
panic("expected all ast.Value types to implement StringLenghter interface, got: " + ValueName(t.Value))
}
func (s String) StringLength() int {
n := 2 // surrounding quotes
bs := util.StringToByteSlice(s)
for i := 0; i < len(bs); {
r, size := utf8.DecodeRune(bs[i:])
switch r {
case '\\', '"':
n += 2 // escaped backslash or quote
case '\b', '\f', '\n', '\r', '\t':
n += 2 // escaped control characters
default:
if r < 0x20 {
n += 6 // unicode escape for other control characters
} else {
n += size // normal rune
}
}
i += size
}
return n
}
func (n Number) StringLength() int {
return len(n)
}
func (b Boolean) StringLength() int {
if b {
return 4
}
return 5
}
func (Null) StringLength() int {
return 4
}
func (s *set) StringLength() int {
if s.Len() == 0 {
return 5 // set()
}
// surrounding {} + ", " for every element - 1
return TermSliceStringLength(s.Slice(), 2) + 2
}
func (a *Array) StringLength() int {
if a.Len() == 0 {
return 2 // []
}
// surrounding brackets + ", " for every element - 1
return TermSliceStringLength(a.elems, 2) + 2
}
func (o *object) StringLength() (n int) {
if o.Len() == 0 {
return 2 // {}
}
// ": " for every item + ", " for every item - 1
o.Foreach(func(key, value *Term) {
n += key.StringLength() + 4 + value.StringLength() // ": " and ", "
})
return n // surrounding {} but also minus last ", "
}
func (ts *TemplateString) StringLength() (n int) {
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
n += 2 + x.StringLength() // for {}
case *Term:
if s, ok := x.Value.(String); ok {
n += len(s) + countUnescapedLeftCurly(string(s))
} else {
n += x.StringLength()
}
default:
n += 9 // <invalid>
}
}
return n + 3 // $"" or $``
}
func (c Call) StringLength() int {
return c[0].StringLength() + 2 + TermSliceStringLength(c[1:], 2)
}
func (r Ref) StringLength() (n int) {
rlen := len(r)
if rlen == 0 {
return 0
}
if s, ok := r[0].Value.(String); ok {
n = len(s) // first term should never be quoted
} else {
n = r[0].StringLength()
}
if rlen == 1 {
return n
}
for _, p := range r[1:] {
switch v := p.Value.(type) {
case String:
str := string(v)
if IsVarCompatibleString(str) && !IsKeyword(str) {
n += 1 + len(str) // dot + name
} else {
n += 2 + p.StringLength() // brackets
}
default:
n += 2 + p.StringLength() // brackets
}
}
return n
}
func (v Var) StringLength() int {
if v.IsWildcard() {
return 1
}
return len(v)
}
func (s *SetComprehension) StringLength() int {
return s.Term.StringLength() + s.Body.StringLength() + 5 // {} and " | "
}
func (a *ArrayComprehension) StringLength() int {
return a.Term.StringLength() + a.Body.StringLength() + 5 // [] and " | "
}
func (o *ObjectComprehension) StringLength() (n int) {
n += o.Key.StringLength()
n += o.Value.StringLength()
n += o.Body.StringLength()
return n + 7 // "{}"", " | ", and ": "
}
func (m *Module) StringLength() (n int) {
if m.Package != nil {
n += m.Package.StringLength() + 2 // newlines
}
if len(m.Imports) > 0 {
for _, imp := range m.Imports {
n += imp.StringLength() + 1 // newline
}
}
if len(m.Rules) > 0 {
for _, rule := range m.Rules {
n += rule.stringLengthWithOpts(toStringOpts{regoVersion: m.regoVersion}) + 1 // newline
}
}
return n
}
func (p *Package) StringLength() int {
if p == nil {
return 21 // <illegal nil package>
}
if len(p.Path) <= 1 {
return 25 + p.Path.StringLength() // // package <illegal path " ... ">
}
return 8 + p.Path[1:].StringLength() // "package ..."
}
func (i *Import) StringLength() (n int) {
n = 7 + i.Path.StringLength() // "import " and path
if i.Alias != "" {
n += 4 + i.Alias.StringLength() // " as " and alias
}
return n
}
func (r *Rule) StringLength() int {
return r.stringLengthWithOpts(toStringOpts{})
}
func (r *Rule) stringLengthWithOpts(opts toStringOpts) int {
n := 0
if r.Default {
n += 8 // "default "
}
n += r.Head.stringLengthWithOpts(opts)
if !r.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
n += 6 // " if { "
default:
n += 3 // " { "
}
n += r.Body.StringLength() + 2 // body and closing " }"
}
if r.Else != nil {
n += r.Else.stringLengthWithOpts(opts)
}
return n
}
func (h *Head) StringLength() int {
return h.stringLengthWithOpts(toStringOpts{})
}
func (h *Head) stringLengthWithOpts(opts toStringOpts) int {
n := h.Reference.StringLength()
containsAdded := false
switch {
case len(h.Args) != 0:
n += h.Args.StringLength()
case len(h.Reference) == 1 && h.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
n += 2 + h.Key.StringLength() // for []
default:
n += 10 + h.Key.StringLength() // " contains "
containsAdded = true
}
}
if h.Value != nil {
if h.Assign {
n += 4 // " := "
} else {
n += 3 // " = "
}
n += h.Value.StringLength()
} else if !containsAdded && h.Name == "" && h.Key != nil {
n += 10 + h.Key.StringLength() // " contains "
}
return n
}
func (a Args) StringLength() (n int) {
n = 2 // ()
for _, t := range a {
n += t.StringLength() + 2 // ", "
}
return n - 2 // minus last ", "
}
func (b Body) StringLength() (n int) {
for _, expr := range b {
n += expr.StringLength() + 2 // "; "
}
return max(n-2, 0) // minus last "; " (if `n` isn't 0)
}
func (e *Expr) StringLength() (n int) {
if e.Negated {
n += 4 // "not "
}
switch terms := e.Terms.(type) {
case []*Term:
if e.IsEquality() && validEqAssignArgCount(e) {
n += terms[1].StringLength() + len(Equality.Infix) + terms[2].StringLength() + 2 // spaces around =
} else {
n += Call(terms).StringLength()
}
case StringLengther:
n += terms.StringLength()
default:
panic(fmt.Sprintf("string length estimation not implemented for type: %T", e.Terms))
}
for _, w := range e.With {
n += w.StringLength() + 1 // space before with
}
return n
}
func (w *With) StringLength() int {
return w.Target.StringLength() + w.Value.StringLength() + 9 // "with " and " as "
}
func (e *Every) StringLength() int {
n := 6 // "every "
if e.Key != nil {
n += e.Key.StringLength() + 2 // ", "
}
n += e.Value.StringLength() + 4 // " in "
n += e.Domain.StringLength() + 3 // " { "
n += e.Body.StringLength() + 2 // " }"
return n
}
func (s *SomeDecl) StringLength() int {
n := 5 // "some "
if call, ok := s.Symbols[0].Value.(Call); ok {
n += 4 // " in "
n += call[1].StringLength()
if len(call) == 4 {
n += 2 // ", "
}
n += call[2].StringLength()
if len(call) == 4 {
n += call[3].StringLength()
}
return n
}
return n + TermSliceStringLength(s.Symbols, 2)
}
func (c *Comment) StringLength() int {
return 1 + len(c.Text) // '#' + text
}
+3 -22
View File
@@ -2,7 +2,6 @@ package ast
import (
"bytes"
"strings"
"sync"
"github.com/open-policy-agent/opa/v1/util"
@@ -12,15 +11,7 @@ var (
TermPtrPool = util.NewSyncPool[Term]()
BytesReaderPool = util.NewSyncPool[bytes.Reader]()
IndexResultPool = util.NewSyncPool[IndexResult]()
bbPool = util.NewSyncPool[bytes.Buffer]()
// Needs custom pool because of custom Put logic.
sbPool = &stringBuilderPool{
pool: sync.Pool{
New: func() any {
return &strings.Builder{}
},
},
}
// Needs custom pool because of custom Put logic.
varVisitorPool = &vvPool{
pool: sync.Pool{
@@ -31,18 +22,8 @@ var (
}
)
type (
stringBuilderPool struct{ pool sync.Pool }
vvPool struct{ pool sync.Pool }
)
func (p *stringBuilderPool) Get() *strings.Builder {
return p.pool.Get().(*strings.Builder)
}
func (p *stringBuilderPool) Put(sb *strings.Builder) {
sb.Reset()
p.pool.Put(sb)
type vvPool struct {
pool sync.Pool
}
func (p *vvPool) Get() *VarVisitor {
+119 -163
View File
@@ -12,7 +12,6 @@ import (
"io"
"math"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
@@ -29,8 +28,6 @@ var (
NullValue Value = Null{}
errFindNotFound = errors.New("find: not found")
varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
)
// Location records a position in source code.
@@ -413,19 +410,24 @@ func (term *Term) IsGround() bool {
return term.Value.IsGround()
}
// termJSON is used to serialize Term to JSON without map allocation.
type termJSON struct {
Location *Location `json:"location,omitempty"`
Type string `json:"type"`
Value Value `json:"value"`
}
// MarshalJSON returns the JSON encoding of the term.
//
// Specialized marshalling logic is required to include a type hint for Value.
func (term *Term) MarshalJSON() ([]byte, error) {
d := map[string]any{
"type": ValueName(term.Value),
"value": term.Value,
d := termJSON{
Type: ValueName(term.Value),
Value: term.Value,
}
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.IncludeLocation.Term {
if term.Location != nil {
d["location"] = term.Location
}
d.Location = term.Location
}
return json.Marshal(d)
}
@@ -925,30 +927,8 @@ func (*TemplateString) IsGround() bool {
}
func (ts *TemplateString) String() string {
str := strings.Builder{}
str.WriteString("$\"")
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
str.WriteByte('{')
str.WriteString(p.String())
str.WriteByte('}')
case *Term:
s := p.String()
if _, ok := x.Value.(String); ok {
s = strings.TrimPrefix(s, "\"")
s = strings.TrimSuffix(s, "\"")
s = EscapeTemplateStringStringPart(s)
}
str.WriteString(s)
default:
str.WriteString("<invalid>")
}
}
str.WriteByte('"')
return str.String()
buf, _ := ts.AppendText(make([]byte, 0, ts.StringLength()))
return util.ByteSliceToString(buf)
}
func TemplateStringTerm(multiLine bool, parts ...Node) *Term {
@@ -973,23 +953,25 @@ func EscapeTemplateStringStringPart(s string) string {
return s
}
l := len(s)
escaped := make([]byte, 0, l+numUnescaped)
return util.ByteSliceToString(AppendEscapedTemplateStringStringPart(make([]byte, 0, len(s)+numUnescaped), s))
}
func AppendEscapedTemplateStringStringPart(buf []byte, s string) []byte {
if s[0] == '{' {
escaped = append(escaped, '\\', s[0])
buf = append(buf, '\\', s[0])
} else {
escaped = append(escaped, s[0])
buf = append(buf, s[0])
}
for i := 1; i < l; i++ {
for i := 1; i < len(s); i++ {
if s[i] == '{' && s[i-1] != '\\' {
escaped = append(escaped, '\\', s[i])
buf = append(buf, '\\', s[i])
} else {
escaped = append(escaped, s[i])
buf = append(buf, s[i])
}
}
return util.ByteSliceToString(escaped)
return buf
}
func countUnescapedLeftCurly(s string) (n int) {
@@ -1340,66 +1322,60 @@ func (ref Ref) Ptr() (string, error) {
return buf.String(), nil
}
// IsVarCompatibleString returns true if s is a valid variable name. String s is a valid variable
// name if it starts with a letter (a-z or A-Z) or underscore (_) and is followed by
// letters (a-z or A-Z), digits (0-9), and underscores.
func IsVarCompatibleString(s string) bool {
return varRegexp.MatchString(s)
l := len(s)
if l == 0 {
return false
}
// not exactly easy on the eyes, but often orders of magnitude faster
// than using a compiled regex (see benchmarks in term_bench_test.go)
is_letter := func(c byte) bool {
return (c > 96 && c < 123) || (c > 64 && c < 91)
}
is_digit := func(c byte) bool {
return c > 47 && c < 58
}
// first character must be a letter or underscore
c := s[0]
if !(is_letter(c) || c == 95) {
return false
}
// remaining characters must be letters, digits, or underscores
for i := 1; i < l; i++ {
if c = s[i]; !(is_letter(c) || is_digit(c) || c == 95) {
return false
}
}
return true
}
func (ref Ref) String() string {
// Note(anderseknert):
// Options tried in the order of cheapness, where after some effort,
// only the last option now requires a (single) allocation:
// 1. empty ref
// 2. single var ref
// 3. built-in function ref
// 4. concatenated parts
reflen := len(ref)
if reflen == 0 {
l := len(ref)
// First check for zero-alloc options, as making the buffer for AppendText
// always costs an allocation.
if l == 0 {
return ""
}
if reflen == 1 {
if l == 1 {
if s, ok := ref[0].Value.(String); ok {
// Ref head should normally be a Var, but if for some reason
// it's a string, don't quote it.
return string(s)
}
return ref[0].Value.String()
}
if name, ok := BuiltinNameFromRef(ref); ok {
return name
}
_var := ref[0].Value.String()
bb := bbPool.Get()
bb.Reset()
defer bbPool.Put(bb)
bb.Grow(len(_var) + len(ref[1:])*7) // rough estimate
bb.WriteString(_var)
for _, p := range ref[1:] {
switch p := p.Value.(type) {
case String:
str := string(p)
if IsVarCompatibleString(str) && !IsKeyword(str) {
bb.WriteByte('.')
bb.WriteString(str)
} else {
bb.WriteByte('[')
// Determine whether we need the full JSON-escaped form
if strings.ContainsFunc(str, isControlOrBackslash) {
bb.Write(strconv.AppendQuote(bb.AvailableBuffer(), str))
} else {
bb.WriteByte('"')
bb.WriteString(str)
bb.WriteByte('"')
}
bb.WriteByte(']')
}
default:
bb.WriteByte('[')
bb.WriteString(p.String())
bb.WriteByte(']')
}
}
return bb.String()
buf, _ := ref.AppendText(make([]byte, 0, ref.StringLength()))
return util.ByteSliceToString(buf)
}
// OutputVars returns a VarSet containing variables that would be bound by evaluating
@@ -1442,6 +1418,15 @@ func NewArray(a ...*Term) *Array {
return arr
}
// NewArrayWithCapacity returns a new empty Array with the given capacity pre-allocated.
func NewArrayWithCapacity(capacity int) *Array {
return &Array{
elems: make([]*Term, 0, capacity),
hashs: make([]int, 0, capacity),
ground: true,
}
}
// Array represents an array as defined by the language. Arrays are similar to the
// same types as defined by JSON with the exception that they can contain Vars
// and References.
@@ -1570,21 +1555,8 @@ func (arr *Array) MarshalJSON() ([]byte, error) {
}
func (arr *Array) String() string {
sb := sbPool.Get()
sb.Grow(len(arr.elems) * 16)
defer sbPool.Put(sb)
sb.WriteByte('[')
for i, e := range arr.elems {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(e.String())
}
sb.WriteByte(']')
return sb.String()
buf, _ := arr.AppendText(make([]byte, 0, arr.StringLength()))
return util.ByteSliceToString(buf)
}
// Len returns the number of elements in the array.
@@ -1702,6 +1674,11 @@ func NewSet(t ...*Term) Set {
return s
}
// NewSetWithCapacity returns a new empty Set with the given capacity pre-allocated.
func NewSetWithCapacity(capacity int) Set {
return newset(capacity)
}
func newset(n int) *set {
var keys []*Term
if n > 0 {
@@ -1765,25 +1742,8 @@ func (s *set) Hash() int {
}
func (s *set) String() string {
if s.Len() == 0 {
return "set()"
}
sb := sbPool.Get()
sb.Grow(s.Len() * 16)
defer sbPool.Put(sb)
sb.WriteByte('{')
for i := range s.sortedKeys() {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(s.keys[i].Value.String())
}
sb.WriteByte('}')
return sb.String()
buf, _ := s.AppendText(make([]byte, 0, s.StringLength()))
return util.ByteSliceToString(buf)
}
func (s *set) sortedKeys() []*Term {
@@ -1824,14 +1784,14 @@ func (s *set) Diff(other Set) Set {
return NewSet()
}
terms := make([]*Term, 0, len(s.keys))
for _, term := range s.sortedKeys() {
result := newset(len(s.keys))
for _, term := range s.keys {
if !other.Contains(term) {
terms = append(terms, term)
result.insert(term, false)
}
}
return NewSet(terms...)
return result
}
// Intersect returns the set containing elements in both s and other.
@@ -1846,21 +1806,28 @@ func (s *set) Intersect(other Set) Set {
n = m
}
terms := make([]*Term, 0, n)
for _, term := range ss.sortedKeys() {
result := newset(n)
for _, term := range ss.keys {
if so.Contains(term) {
terms = append(terms, term)
result.insert(term, false)
}
}
return NewSet(terms...)
return result
}
// Union returns the set containing all elements of s and other.
func (s *set) Union(other Set) Set {
r := NewSet()
s.Foreach(r.Add)
other.Foreach(r.Add)
o := other.(*set)
// Pre-allocate with max size - avoids over-allocation for overlapping sets
// while only requiring one potential grow for disjoint sets.
r := newset(max(len(s.keys), len(o.keys)))
for _, term := range s.keys {
r.insert(term, false)
}
for _, term := range o.keys {
r.insert(term, false)
}
return r
}
@@ -2034,6 +2001,11 @@ func NewObject(t ...[2]*Term) Object {
return obj
}
// NewObjectWithCapacity returns a new empty Object with the given capacity pre-allocated.
func NewObjectWithCapacity(capacity int) Object {
return newobject(capacity)
}
// ObjectTerm creates a new Term with an Object value.
func ObjectTerm(o ...[2]*Term) *Term {
return &Term{Value: NewObject(o...)}
@@ -2554,24 +2526,8 @@ func (obj *object) Len() int {
}
func (obj *object) String() string {
sb := sbPool.Get()
sb.Grow(obj.Len() * 32)
defer sbPool.Put(sb)
sb.WriteByte('{')
for i, elem := range obj.sortedKeys() {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(elem.key.String())
sb.WriteString(": ")
sb.WriteString(elem.value.String())
}
sb.WriteByte('}')
return sb.String()
buf, _ := obj.AppendText(make([]byte, 0, obj.StringLength()))
return util.ByteSliceToString(buf)
}
func (*object) get(*Term) *objectElem {
@@ -2642,7 +2598,7 @@ func filterObject(o Value, filter Value) (Value, error) {
case String, Number, Boolean, Null:
return o, nil
case *Array:
values := NewArray()
values := make([]*Term, 0, v.Len())
for i := range v.Len() {
subFilter := filteredObj.Get(InternedIntegerString(i))
if subFilter != nil {
@@ -2650,10 +2606,10 @@ func filterObject(o Value, filter Value) (Value, error) {
if err != nil {
return nil, err
}
values = values.Append(NewTerm(filteredValue))
values = append(values, NewTerm(filteredValue))
}
}
return values, nil
return NewArray(values...), nil
case Set:
terms := make([]*Term, 0, v.Len())
for _, t := range v.Slice() {
@@ -2776,7 +2732,8 @@ func (ac *ArrayComprehension) IsGround() bool {
}
func (ac *ArrayComprehension) String() string {
return "[" + ac.Term.String() + " | " + ac.Body.String() + "]"
buf, _ := ac.AppendText(make([]byte, 0, ac.StringLength()))
return util.ByteSliceToString(buf)
}
// ObjectComprehension represents an object comprehension as defined in the language.
@@ -2836,7 +2793,8 @@ func (oc *ObjectComprehension) IsGround() bool {
}
func (oc *ObjectComprehension) String() string {
return "{" + oc.Key.String() + ": " + oc.Value.String() + " | " + oc.Body.String() + "}"
buf, _ := oc.AppendText(make([]byte, 0, oc.StringLength()))
return util.ByteSliceToString(buf)
}
// SetComprehension represents a set comprehension as defined in the language.
@@ -2893,7 +2851,8 @@ func (sc *SetComprehension) IsGround() bool {
}
func (sc *SetComprehension) String() string {
return "{" + sc.Term.String() + " | " + sc.Body.String() + "}"
buf, _ := sc.AppendText(make([]byte, 0, sc.StringLength()))
return util.ByteSliceToString(buf)
}
// Call represents as function call in the language.
@@ -2954,11 +2913,8 @@ func (c Call) Operands() []*Term {
}
func (c Call) String() string {
args := make([]string, len(c)-1)
for i := 1; i < len(c); i++ {
args[i-1] = c[i].String()
}
return fmt.Sprintf("%v(%v)", c[0], strings.Join(args, ", "))
buf, _ := c.AppendText(make([]byte, 0, c.StringLength()))
return util.ByteSliceToString(buf)
}
func termSliceCopy(a []*Term) []*Term {
+266
View File
@@ -0,0 +1,266 @@
package ast
import (
"encoding"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
// AppendText appends the text representation of term (i.e. as printed in policy) to
// buf and returns the extended buffer.
func (term *Term) AppendText(buf []byte) ([]byte, error) {
if app, ok := term.Value.(encoding.TextAppender); ok {
return app.AppendText(buf)
}
return append(buf, term.Value.String()...), nil
}
func (v Var) AppendText(buf []byte) ([]byte, error) {
if v.IsWildcard() {
return append(buf, WildcardString...), nil
}
return append(buf, v...), nil
}
func (b Boolean) AppendText(buf []byte) ([]byte, error) {
if b {
return append(buf, "true"...), nil
}
return append(buf, "false"...), nil
}
func (Null) AppendText(buf []byte) ([]byte, error) {
return append(buf, "null"...), nil
}
func (str String) AppendText(buf []byte) ([]byte, error) {
return strconv.AppendQuote(buf, string(str)), nil
}
func (str String) appendNoQuote(buf []byte) []byte {
// Append using strconv.AppendQuote for proper escaping, but trim off
// the leading and trailing quotes afterwards.
oldLen := len(buf)
buf = strconv.AppendQuote(buf, string(str))
newLen := len(buf)
quoted := buf[oldLen:newLen]
return append(buf[:oldLen], quoted[1:len(quoted)-1]...)
}
func (num Number) AppendText(buf []byte) ([]byte, error) {
return append(buf, num...), nil
}
func (arr *Array) AppendText(buf []byte) ([]byte, error) {
buf, err := AppendDelimeted(append(buf, '['), arr.elems, ", ")
if err != nil {
return nil, err
}
return append(buf, ']'), nil
}
func (obj *object) AppendText(buf []byte) ([]byte, error) {
olen := obj.Len()
if olen == 0 {
return append(buf, "{}"...), nil
}
buf = append(buf, '{')
var err error
// first key-value pair
keys := obj.sortedKeys()
for i := range keys {
if buf, err = keys[i].key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ": "...)
if buf, err = keys[i].value.AppendText(buf); err != nil {
return nil, err
}
if i < olen-1 {
buf = append(buf, ", "...)
}
}
return append(buf, '}'), nil
}
func (obj *lazyObj) AppendText(buf []byte) ([]byte, error) {
return append(buf, obj.force().String()...), nil
}
func (s *set) AppendText(buf []byte) ([]byte, error) {
slen := s.Len()
if slen == 0 {
return append(buf, "set()"...), nil
}
var err error
buf = append(buf, '{')
if buf, err = AppendDelimeted(buf, s.sortedKeys(), ", "); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
func (c Call) AppendText(buf []byte) ([]byte, error) {
if len(c) == 0 {
return buf, nil
}
var err error
if buf, err = c[0].AppendText(buf); err != nil {
return nil, err
}
if buf, err = AppendDelimeted(append(buf, '('), c[1:], ", "); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
func (ts *TemplateString) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "$\""...)
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
buf = append(buf, '{')
var err error
if buf, err = x.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, '}')
case *Term:
if str, ok := x.Value.(String); ok {
// TODO(anders): this is a bit of a mess, but as explained by the comment on
// [EscapeTemplateStringStringPart], required as long as we rely on strconv for escaping, which adds
// quotes around the string that we don't want here, and trying to "unappend" them is not nice at all..
s := string(str)
ulc := countUnescapedLeftCurly(s)
sl := str.StringLength() + ulc - 2 // no surrounding quotes
if sl == len(s) { // no escaping needed
buf = append(buf, s...)
} else { // some escaping needed
if sl == len(s)+ulc { // only unescaped {
buf = AppendEscapedTemplateStringStringPart(buf, string(str))
} else { // full escaping needed. this is expensive but luckily rare
tmp := str.appendNoQuote(make([]byte, 0, sl))
ets := EscapeTemplateStringStringPart(util.ByteSliceToString(tmp))
buf = append(buf, ets...)
}
}
} else {
var err error
if buf, err = x.AppendText(buf); err != nil {
return nil, err
}
}
default:
buf = append(buf, "<invalid>"...)
}
}
return append(buf, '"'), nil
}
func (r Ref) AppendText(buf []byte) ([]byte, error) {
reflen := len(r)
if reflen == 0 {
return buf, nil
}
if reflen == 1 {
if s, ok := r[0].Value.(String); ok {
// While a ref head is typically a Var, a lone String term should not be quoted
return append(buf, s...), nil
}
return r[0].AppendText(buf)
}
if name, ok := BuiltinNameFromRef(r); ok {
return append(buf, name...), nil
}
var err error
if s, ok := r[0].Value.(String); ok {
buf = append(buf, s...)
} else if buf, err = r[0].AppendText(buf); err != nil {
return nil, err
}
for _, p := range r[1:] {
switch v := p.Value.(type) {
case String:
str := string(v)
if IsVarCompatibleString(str) && !IsKeyword(str) {
buf = append(append(buf, '.'), str...)
} else {
buf = append(buf, '[')
// Determine whether we need the full JSON-escaped form
if strings.ContainsFunc(str, isControlOrBackslash) {
if buf, err = v.AppendText(buf); err != nil {
return nil, err
}
} else {
buf = append(append(append(buf, '"'), str...), '"')
}
buf = append(buf, ']')
}
default:
buf = append(buf, '[')
if buf, err = p.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ']')
}
}
return buf, nil
}
func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
if buf, err = sc.Term.AppendText(buf); err != nil {
return nil, err
}
if buf, err = sc.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '[')
var err error
if buf, err = ac.Term.AppendText(buf); err != nil {
return nil, err
}
if buf, err = ac.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, ']'), nil
}
func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
if buf, err = oc.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ": "...)
if buf, err = oc.Value.AppendText(buf); err != nil {
return nil, err
}
if buf, err = oc.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
+5
View File
@@ -25,6 +25,11 @@
"Minor": 17,
"Patch": 0
},
"array.flatten": {
"Major": 1,
"Minor": 13,
"Patch": 0
},
"array.reverse": {
"Major": 0,
"Minor": 36,
+1 -2
View File
@@ -14,7 +14,6 @@ import (
"fmt"
"hash"
"io"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
@@ -132,5 +131,5 @@ func encodePrimitive(v any) []byte {
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
_ = encoder.Encode(v)
return []byte(strings.Trim(buf.String(), "\n"))
return bytes.Trim(buf.Bytes(), "\n")
}
+1 -3
View File
@@ -9,7 +9,6 @@ import (
"bytes"
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strings"
@@ -29,7 +28,6 @@ const defaultLocationFile = "__format_default__"
var (
expandedConst = ast.NewBody(ast.NewExpr(ast.InternedTerm(true)))
commentsSlicePool = util.NewSlicePool[*ast.Comment](50)
varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
)
// Opts lets you control the code formatting via `AstWithOpts()`.
@@ -1441,7 +1439,7 @@ func (w *writer) writeRefStringPath(s ast.String, l *ast.Location) {
}
func (w *writer) shouldBracketRefTerm(s string, l *ast.Location) bool {
if !varRegexp.MatchString(s) {
if !ast.IsVarCompatibleString(s) {
return true
}
+38 -31
View File
@@ -15,7 +15,7 @@ import (
"sync"
"time"
"github.com/open-policy-agent/opa/internal/report"
"github.com/open-policy-agent/opa/internal/versioncheck"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/sdk/trace"
@@ -140,6 +140,9 @@ const (
type TriggerMode string
const (
// TriggerImmediate represents uploading chunks when ready, flushed by the periodic polling mechanism
TriggerImmediate TriggerMode = "immediate"
// TriggerPeriodic represents periodic polling mechanism
TriggerPeriodic TriggerMode = "periodic"
@@ -212,11 +215,10 @@ type Manager struct {
tracerProvider *trace.TracerProvider
distributedTacingOpts tracing.Options
registeredNDCacheTriggers []func(bool)
registeredTelemetryGatherers map[string]report.Gatherer
bootstrapConfigLabels map[string]string
hooks hooks.Hooks
enableTelemetry bool
reporter report.Reporter
enableVersionCheck bool
versionChecker versioncheck.Checker
opaReportNotifyCh chan struct{}
stop chan chan struct{}
parserOptions ast.ParserOptions
@@ -272,10 +274,10 @@ func getWasmResolversOnContext(context *storage.Context) []*wasm.Resolver {
func validateTriggerMode(mode TriggerMode) error {
switch mode {
case TriggerPeriodic, TriggerManual:
case TriggerPeriodic, TriggerManual, TriggerImmediate:
return nil
default:
return fmt.Errorf("invalid trigger mode %q (want %q or %q)", mode, TriggerPeriodic, TriggerManual)
return fmt.Errorf("invalid trigger mode %q (want %q, %q or %q)", mode, TriggerPeriodic, TriggerManual, TriggerImmediate)
}
}
@@ -418,18 +420,36 @@ func WithParserOptions(opts ast.ParserOptions) func(*Manager) {
}
}
// WithEnableTelemetry controls whether OPA will send telemetry reports to an external service.
func WithEnableTelemetry(enableTelemetry bool) func(*Manager) {
// WithEnableVersionCheck controls whether OPA will check for version updates.
func WithEnableVersionCheck(enable bool) func(*Manager) {
return func(m *Manager) {
m.enableTelemetry = enableTelemetry
m.enableVersionCheck = enable
}
}
// WithEnableTelemetry controls whether OPA will check for version updates.
//
// Deprecated: please use WithEnableVersionCheck instead.
func WithEnableTelemetry(enableTelemetry bool) func(*Manager) {
return WithEnableVersionCheck(enableTelemetry)
}
// WithTelemetryGatherers allows registration of telemetry gatherers which enable injection of additional data in the
// telemetry report
func WithTelemetryGatherers(gs map[string]report.Gatherer) func(*Manager) {
//
// Deprecated: This function is deprecated as telemetry gathering has been removed. Use WithVersionChecker to provide
// a custom version checker implementation if needed.
func WithTelemetryGatherers(gs map[string]versioncheck.Gatherer) func(*Manager) {
return func(m *Manager) {
m.registeredTelemetryGatherers = gs
// No-op: telemetry gatherers are no longer used
}
}
// WithVersionChecker sets a custom version checker implementation.
// If not provided, a default GitHub-based version checker will be used when telemetry is enabled.
func WithVersionChecker(checker versioncheck.Checker) func(*Manager) {
return func(m *Manager) {
m.versionChecker = checker
}
}
@@ -507,25 +527,12 @@ func New(raw []byte, id string, store storage.Store, opts ...func(*Manager)) (*M
return nil, err
}
if m.enableTelemetry {
reporter, err := report.New(report.Options{Logger: m.logger})
if m.enableVersionCheck {
versionChecker, err := versioncheck.New(versioncheck.Options{Logger: m.logger})
if err != nil {
return nil, err
}
m.reporter = reporter
m.reporter.RegisterGatherer("min_compatible_version", func(_ context.Context) (any, error) {
var minimumCompatibleVersion string
if c := m.GetCompiler(); c != nil && c.Required != nil {
minimumCompatibleVersion, _ = c.Required.MinimumCompatibleVersion()
}
return minimumCompatibleVersion, nil
})
// register any additional gatherers
for k, g := range m.registeredTelemetryGatherers {
m.reporter.RegisterGatherer(k, g)
}
m.versionChecker = versionChecker
}
return m, nil
@@ -543,7 +550,7 @@ func (m *Manager) Init(ctx context.Context) error {
Context: storage.NewContext(),
}
if m.enableTelemetry {
if m.enableVersionCheck {
m.opaReportNotifyCh = make(chan struct{})
m.stop = make(chan chan struct{})
go m.sendOPAUpdateLoop(ctx)
@@ -969,7 +976,7 @@ func (m *Manager) onCommit(ctx context.Context, txn storage.Transaction, event s
if compiler != nil {
m.setCompiler(compiler)
if m.enableTelemetry && event.PolicyChanged() {
if m.enableVersionCheck && event.PolicyChanged() {
m.opaReportNotifyCh <- struct{}{}
}
@@ -1173,9 +1180,9 @@ func (m *Manager) sendOPAUpdateLoop(ctx context.Context) {
if opaReportNotify {
opaReportNotify = false
_, err := m.reporter.SendReport(ctx)
_, err := m.versionChecker.LatestVersion(ctx)
if err != nil {
m.logger.WithFields(map[string]any{"err": err}).Debug("Unable to send OPA telemetry report.")
m.logger.WithFields(map[string]any{"err": err}).Debug("Unable to check OPA version.")
}
}
+1 -1
View File
@@ -21,9 +21,9 @@ import (
"strings"
"time"
"github.com/go-ini/ini"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/v1/logging"
"gopkg.in/ini.v1"
)
const (
+10
View File
@@ -1080,6 +1080,16 @@ func Store(s storage.Store) func(r *Rego) {
}
}
// Data returns an argument that sets the Rego data document. Data should be
// a map representing the data document. This is a simpler alternative to
// using Store with inmem.NewFromObject for cases where an in-memory store
// with static data is sufficient.
func Data(x map[string]any) func(r *Rego) {
return func(r *Rego) {
r.store = inmem.NewFromObject(x)
}
}
// StoreReadAST returns an argument that sets whether the store should eagerly convert data to AST values.
//
// Only applicable when no store has been set on the Rego object through the Store option.
+8 -4
View File
@@ -45,12 +45,13 @@ func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
// Non-integer values found, so we need to sum as floats.
sum := big.NewFloat(0)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
n, ok := x.Value.(ast.Number)
if !ok {
return builtins.NewOperandElementErr(1, a, x.Value, "number")
}
sum = new(big.Float).Add(sum, builtins.NumberToFloat(n))
sum = new(big.Float).Add(sum, builtins.NumberToFloatInto(tmp, n))
return nil
})
if err != nil {
@@ -74,12 +75,13 @@ func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
}
sum := big.NewFloat(0)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
n, ok := x.Value.(ast.Number)
if !ok {
return builtins.NewOperandElementErr(1, a, x.Value, "number")
}
sum = new(big.Float).Add(sum, builtins.NumberToFloat(n))
sum = new(big.Float).Add(sum, builtins.NumberToFloatInto(tmp, n))
return nil
})
if err != nil {
@@ -94,12 +96,13 @@ func builtinProduct(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
switch a := operands[0].Value.(type) {
case *ast.Array:
product := big.NewFloat(1)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
n, ok := x.Value.(ast.Number)
if !ok {
return builtins.NewOperandElementErr(1, a, x.Value, "number")
}
product = new(big.Float).Mul(product, builtins.NumberToFloat(n))
product = new(big.Float).Mul(product, builtins.NumberToFloatInto(tmp, n))
return nil
})
if err != nil {
@@ -108,12 +111,13 @@ func builtinProduct(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
return iter(ast.NewTerm(builtins.FloatToNumber(product)))
case ast.Set:
product := big.NewFloat(1)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
n, ok := x.Value.(ast.Number)
if !ok {
return builtins.NewOperandElementErr(1, a, x.Value, "number")
}
product = new(big.Float).Mul(product, builtins.NumberToFloat(n))
product = new(big.Float).Mul(product, builtins.NumberToFloatInto(tmp, n))
return nil
})
if err != nil {
+46 -1
View File
@@ -43,6 +43,42 @@ func builtinArrayConcat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.T
return iter(ast.ArrayTerm(arrC...))
}
func builtinArrayFlatten(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
arr, err := builtins.ArrayOperand(operands[0].Value, 1)
if err != nil {
return err
}
size := arr.Len()
preAlloc := size
containsArray := false
for i := range size {
if nested, ok := arr.Elem(i).Value.(*ast.Array); ok {
containsArray = true
preAlloc += nested.Len() - 1
}
}
if !containsArray && size == preAlloc {
return iter(operands[0]) // Empty array, or no nested arrays -> nothing to flatten.
}
flattened := make([]*ast.Term, 0, preAlloc)
for i := range size {
elem := arr.Elem(i)
if nested, ok := elem.Value.(*ast.Array); ok {
for j := range nested.Len() {
flattened = append(flattened, nested.Elem(j))
}
} else {
flattened = append(flattened, elem)
}
}
return iter(ast.ArrayTerm(flattened...))
}
func builtinArraySlice(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
arr, err := builtins.ArrayOperand(operands[0].Value, 1)
if err != nil {
@@ -89,8 +125,16 @@ func builtinArrayReverse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
}
length := arr.Len()
reversedArr := make([]*ast.Term, length)
if length == 0 {
return iter(ast.InternedEmptyArray)
}
if length == 1 {
return iter(operands[0])
}
reversedArr := make([]*ast.Term, length)
for index := range length {
reversedArr[index] = arr.Elem(length - index - 1)
}
@@ -100,6 +144,7 @@ func builtinArrayReverse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
func init() {
RegisterBuiltinFunc(ast.ArrayConcat.Name, builtinArrayConcat)
RegisterBuiltinFunc(ast.ArrayFlatten.Name, builtinArrayFlatten)
RegisterBuiltinFunc(ast.ArraySlice.Name, builtinArraySlice)
RegisterBuiltinFunc(ast.ArrayReverse.Name, builtinArrayReverse)
}
+10 -3
View File
@@ -251,11 +251,18 @@ func ArrayOperand(x ast.Value, pos int) (*ast.Array, error) {
// NumberToFloat converts n to a big float.
func NumberToFloat(n ast.Number) *big.Float {
r, ok := new(big.Float).SetString(string(n))
if !ok {
return NumberToFloatInto(nil, n)
}
// NumberToFloatInto converts n to a big float, storing it in dst when provided.
func NumberToFloatInto(dst *big.Float, n ast.Number) *big.Float {
if dst == nil {
dst = new(big.Float)
}
if _, ok := dst.SetString(string(n)); !ok {
panic("illegal value")
}
return r
return dst
}
// FloatToNumber converts f to a number.
+1 -1
View File
@@ -300,7 +300,7 @@ func builtinNetCIDRMerge(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
merged := evalNetCIDRMerge(networks)
result := ast.NewSet()
result := ast.NewSetWithCapacity(len(merged))
for _, network := range merged {
result.Add(ast.StringTerm(network.String()))
}
+1 -1
View File
@@ -287,7 +287,7 @@ func builtinURLQueryDecodeObject(_ BuiltinContext, operands []*ast.Term, iter fu
return err
}
queryObject := ast.NewObject()
queryObject := ast.NewObjectWithCapacity(len(queryParams))
for k, v := range queryParams {
paramsArray := make([]*ast.Term, len(v))
for i, param := range v {
+20 -6
View File
@@ -6,9 +6,9 @@ package topdown
import (
"errors"
"fmt"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
// Halt is a special error type that built-in function implementations return to indicate
@@ -82,13 +82,27 @@ func (e *Error) Is(target error) bool {
}
func (e *Error) Error() string {
msg := fmt.Sprintf("%v: %v", e.Code, e.Message)
buf, _ := e.AppendText(make([]byte, 0, e.StringLength()))
return util.ByteSliceToString(buf)
}
func (e *Error) AppendText(buf []byte) ([]byte, error) {
if e.Location != nil {
msg = e.Location.String() + ": " + msg
buf, _ := e.Location.AppendText(buf)
buf = append(append(buf, ": "...), e.Code...)
buf = append(append(buf, ": "...), e.Message...)
return buf, nil
}
return msg
return append(append(append(buf, e.Code...), ": "...), e.Message...), nil
}
func (e *Error) StringLength() int {
l := len(e.Code) + 2 + len(e.Message)
if e.Location != nil {
l += e.Location.StringLength() + 2
}
return l
}
func (e *Error) Wrap(err error) *Error {
@@ -124,11 +138,11 @@ func objectDocKeyConflictErr(loc *ast.Location) error {
}
}
func unsupportedBuiltinErr(loc *ast.Location) error {
func unsupportedBuiltinErr(loc *ast.Location, name string) error {
return &Error{
Code: InternalErr,
Location: loc,
Message: "unsupported built-in",
Message: "unsupported built-in: " + name,
}
}
+36 -20
View File
@@ -969,7 +969,7 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
builtinName := ref.String()
bi, f, ok := e.builtinFunc(builtinName)
if !ok {
return unsupportedBuiltinErr(e.query[e.index].Location)
return unsupportedBuiltinErr(e.query[e.index].Location, builtinName)
}
if mocked { // value replacement of built-in call
@@ -1492,20 +1492,20 @@ func (e *eval) amendComprehension(a *ast.Term, b1 *bindings) (*ast.Term, error)
}
func (e *eval) biunifyComprehensionArray(x *ast.ArrayComprehension, b *ast.Term, b1, b2 *bindings, iter unifyIterator) error {
result := ast.NewArray()
var elements []*ast.Term
child := evalPool.Get()
e.closure(x.Body, child)
defer evalPool.Put(child)
err := child.Run(func(child *eval) error {
result = result.Append(child.bindings.Plug(x.Term))
elements = append(elements, child.bindings.Plug(x.Term))
return nil
})
if err != nil {
return err
}
return e.biunify(ast.NewTerm(result), b, b1, b2, iter)
return e.biunify(ast.NewTerm(ast.NewArray(elements...)), b, b1, b2, iter)
}
func (e *eval) biunifyComprehensionSet(x *ast.SetComprehension, b *ast.Term, b1, b2 *bindings, iter unifyIterator) error {
@@ -2486,6 +2486,20 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error {
return cpy.eval(iter)
}
// enumerateNext is a helper to avoid closure allocation in enumerate loops.
// Method values don't allocate, unlike explicit closures.
// Using a pointer to evalTree avoids copying the 96-byte structure.
// Fields are ordered by size for optimal memory alignment (16 > 8 > 8 bytes).
type enumerateNext struct {
iter unifyIterator // 16 bytes (interface)
e *evalTree // 8 bytes (pointer)
key *ast.Term // 8 bytes (pointer)
}
func (en *enumerateNext) call() error {
return en.e.next(en.iter, en.key)
}
func (e evalTree) enumerate(iter unifyIterator) error {
if e.e.inliningControl.Disabled(e.plugged[:e.pos], true) {
@@ -2501,14 +2515,17 @@ func (e evalTree) enumerate(iter unifyIterator) error {
dc.deferred = nil
defer deecPool.Put(dc)
// Use method value to avoid closure allocation.
// Create once and reuse for both doc and virtual doc enumeration.
en := enumerateNext{iter: iter, e: &e, key: nil}
if doc != nil {
switch doc := doc.(type) {
case *ast.Array:
for i := range doc.Len() {
k := ast.InternedTerm(i)
err := e.e.biunify(k, e.ref[e.pos], e.bindings, e.bindings, func() error {
return e.next(iter, k)
})
en.key = k
err := e.e.biunify(k, e.ref[e.pos], e.bindings, e.bindings, en.call)
if err := dc.handleErr(err); err != nil {
return err
@@ -2517,21 +2534,20 @@ func (e evalTree) enumerate(iter unifyIterator) error {
case ast.Object:
ki := doc.KeysIterator()
for k, more := ki.Next(); more; k, more = ki.Next() {
err := e.e.biunify(k, e.ref[e.pos], e.bindings, e.bindings, func() error {
return e.next(iter, k)
})
en.key = k
err := e.e.biunify(k, e.ref[e.pos], e.bindings, e.bindings, en.call)
if err := dc.handleErr(err); err != nil {
return err
}
}
case ast.Set:
if err := doc.Iter(func(elem *ast.Term) error {
err := e.e.biunify(elem, e.ref[e.pos], e.bindings, e.bindings, func() error {
return e.next(iter, elem)
})
return dc.handleErr(err)
}); err != nil {
return err
// Use Slice() to avoid closure allocation in Iter()
for _, elem := range doc.Slice() {
en.key = elem
err := e.e.biunify(elem, e.ref[e.pos], e.bindings, e.bindings, en.call)
if err := dc.handleErr(err); err != nil {
return err
}
}
}
}
@@ -2544,11 +2560,11 @@ func (e evalTree) enumerate(iter unifyIterator) error {
return nil
}
// Reuse the same enumerateNext for virtual documents
for _, k := range e.node.Sorted {
key := ast.NewTerm(k)
if err := e.e.biunify(key, e.ref[e.pos], e.bindings, e.bindings, func() error {
return e.next(iter, key)
}); err != nil {
en.key = key
if err := e.e.biunify(key, e.ref[e.pos], e.bindings, e.bindings, en.call); err != nil {
return err
}
}
+2 -2
View File
@@ -146,7 +146,7 @@ func pruneIrrelevantGraphQLASTNodes(value ast.Value) ast.Value {
// extant ast type!
switch x := value.(type) {
case *ast.Array:
result := ast.NewArray()
result := ast.NewArrayWithCapacity(x.Len())
// Iterate over the array's elements, and do the following:
// - Drop any Nulls
// - Drop any any empty object/array value (after running the pruner)
@@ -173,7 +173,7 @@ func pruneIrrelevantGraphQLASTNodes(value ast.Value) ast.Value {
}
return result
case ast.Object:
result := ast.NewObject()
result := ast.NewObjectWithCapacity(x.Len())
// Iterate over our object's keys, and do the following:
// - Drop "Position".
// - Drop any key with a Null value.
+6 -6
View File
@@ -65,7 +65,7 @@ func jsonRemove(a *ast.Term, b *ast.Term) (*ast.Term, error) {
case ast.String, ast.Number, ast.Boolean, ast.Null:
return a, nil
case ast.Object:
newObj := ast.NewObject()
newObj := ast.NewObjectWithCapacity(aValue.Len())
err := aValue.Iter(func(k *ast.Term, v *ast.Term) error {
// recurse and add the diff of sub objects as needed
diffValue, err := jsonRemove(v, bObj.Get(k))
@@ -80,7 +80,7 @@ func jsonRemove(a *ast.Term, b *ast.Term) (*ast.Term, error) {
}
return ast.NewTerm(newObj), nil
case ast.Set:
newSet := ast.NewSet()
newSet := ast.NewSetWithCapacity(aValue.Len())
err := aValue.Iter(func(v *ast.Term) error {
// recurse and add the diff of sub objects as needed
diffValue, err := jsonRemove(v, bObj.Get(v))
@@ -97,7 +97,7 @@ func jsonRemove(a *ast.Term, b *ast.Term) (*ast.Term, error) {
case *ast.Array:
// When indexes are removed we shift left to close empty spots in the array
// as per the JSON patch spec.
newArray := ast.NewArray()
newArraySlice := make([]*ast.Term, 0, aValue.Len())
for i := range aValue.Len() {
v := aValue.Elem(i)
// recurse and add the diff of sub objects as needed
@@ -107,10 +107,10 @@ func jsonRemove(a *ast.Term, b *ast.Term) (*ast.Term, error) {
return nil, err
}
if diffValue != nil {
newArray = newArray.Append(diffValue)
newArraySlice = append(newArraySlice, diffValue)
}
}
return ast.NewTerm(newArray), nil
return ast.NewTerm(ast.NewArray(newArraySlice...)), nil
default:
return nil, fmt.Errorf("invalid value type %T", a)
}
@@ -196,7 +196,7 @@ func parsePath(path *ast.Term) (ast.Ref, error) {
}
func pathsToObject(paths []ast.Ref) ast.Object {
root := ast.NewObject()
root := ast.NewObjectWithCapacity(len(paths))
for _, path := range paths {
node := root
+2 -2
View File
@@ -27,7 +27,7 @@ func astValueToJSONSchemaLoader(value ast.Value) (gojsonschema.JSONLoader, error
return nil, errors.New("invalid JSON string")
}
loader = gojsonschema.NewStringLoader(string(x))
case ast.Object:
case ast.Object, *ast.Array:
// In case of object serialize it to JSON representation.
var data any
data, err = ast.JSON(value)
@@ -110,7 +110,7 @@ func builtinJSONMatchSchema(bctx BuiltinContext, operands []*ast.Term, iter func
}
// In case of validation errors produce Rego array of objects to describe the errors.
arr := ast.NewArray()
arr := ast.NewArrayWithCapacity(len(result.Errors()))
for _, re := range result.Errors() {
o := ast.NewObject(
[...]*ast.Term{ast.StringTerm("error"), ast.StringTerm(re.String())},
+1 -1
View File
@@ -49,7 +49,7 @@ func builtinLookupIPAddr(bctx BuiltinContext, operands []*ast.Term, iter func(*a
return err
}
ret := ast.NewSet()
ret := ast.NewSetWithCapacity(len(addrs))
for _, a := range addrs {
ret.Add(ast.StringTerm(a.String()))
+1 -1
View File
@@ -96,7 +96,7 @@ func canGenerateCheapRange(operands []*ast.Term) bool {
func canGenerateCheapRangeStep(operands []*ast.Term) bool {
if canGenerateCheapRange(operands) {
step, err := builtins.IntOperand(operands[1].Value, 3)
step, err := builtins.IntOperand(operands[2].Value, 3)
if err == nil && ast.HasInternedIntNumberTerm(step) {
return true
}
+23 -8
View File
@@ -52,13 +52,21 @@ func builtinObjectUnionN(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
// Example:
// Input: [{"a": {"b": 2}}, {"a": 4}, {"a": {"c": 3}}]
// Want Output: {"a": {"c": 3}}
result := ast.NewObject()
frozenKeys := map[*ast.Term]struct{}{}
for i := arr.Len() - 1; i >= 0; i-- {
// First pass: count total keys for pre-allocation
totalSize := 0
for i := range arr.Len() {
o, ok := arr.Elem(i).Value.(ast.Object)
if !ok {
return builtins.NewOperandElementErr(1, arr, arr.Elem(i).Value, "object")
}
totalSize += o.Len()
}
result := ast.NewObjectWithCapacity(totalSize)
frozenKeys := make(map[*ast.Term]struct{}, totalSize)
for i := arr.Len() - 1; i >= 0; i-- {
o := arr.Elem(i).Value.(ast.Object) // Already validated above
mergewithOverwriteInPlace(result, o, frozenKeys)
}
@@ -77,7 +85,9 @@ func builtinObjectRemove(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
if err != nil {
return err
}
r := ast.NewObject()
// Pre-allocate with obj size (upper bound for result)
r := ast.NewObjectWithCapacity(obj.Len())
obj.Foreach(func(key *ast.Term, value *ast.Term) {
if !keysToRemove.Contains(key) {
r.Insert(key, value)
@@ -100,7 +110,8 @@ func builtinObjectFilter(_ BuiltinContext, operands []*ast.Term, iter func(*ast.
return err
}
filterObj := ast.NewObject()
// Pre-allocate with keys size (upper bound for filter object)
filterObj := ast.NewObjectWithCapacity(keys.Len())
keys.Foreach(func(key *ast.Term) {
filterObj.Insert(key, ast.InternedNullTerm)
})
@@ -158,15 +169,19 @@ func builtinObjectKeys(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Te
}
// getObjectKeysParam returns a set of key values
// from a supplied ast array, object, set value
// from a supplied ast array, object, set value.
// The returned set must not be mutated. For Set
// inputs, it may be the original.
func getObjectKeysParam(arrayOrSet ast.Value) (ast.Set, error) {
switch v := arrayOrSet.(type) {
case *ast.Array:
keys := ast.NewSet()
keys := ast.NewSetWithCapacity(v.Len())
v.Foreach(keys.Add)
return keys, nil
case ast.Set:
return ast.NewSet(v.Slice()...), nil
// Return directly. Callers only use this for Contains() checks
// without mutating the set.
return v, nil
case ast.Object:
return ast.NewSet(v.Keys()...), nil
}
+35 -1
View File
@@ -266,7 +266,41 @@ func builtinRegexReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*a
return err
}
res := re.ReplaceAllString(string(base), string(value))
// If no cancellation context, use the fast path
if bctx.Cancel == nil {
res := re.ReplaceAllString(string(base), string(value))
if res == string(base) {
return iter(operands[0])
}
return iter(ast.InternedTerm(res))
}
// Use sink writer for cancellation-aware replacement
sink := newSink(ast.RegexReplace.Name, len(base), bctx.Cancel)
src := []byte(base)
repl := []byte(value)
// Find all matches at once to preserve anchor behavior: replace("foo", "^[a-z]", "F") => "Foo"
allMatches := re.FindAllSubmatchIndex(src, -1)
lastEnd := 0
for _, match := range allMatches {
if _, err := sink.Write(src[lastEnd:match[0]]); err != nil {
return err
}
if _, err := sink.Write(re.Expand(nil, repl, src, match)); err != nil {
return err
}
lastEnd = match[1]
}
if _, err := sink.Write(src[lastEnd:]); err != nil {
return err
}
res := sink.String()
if res == string(base) {
return iter(operands[0])
}
+1 -1
View File
@@ -99,7 +99,7 @@ func (t *resolverTrie) mktree(e *eval, in resolver.Input) (ast.Value, error) {
}
return result.Value, nil
}
obj := ast.NewObject()
obj := ast.NewObjectWithCapacity(len(t.children))
for k, child := range t.children {
v, err := child.mktree(e, resolver.Input{Ref: append(in.Ref, ast.NewTerm(k)), Input: in.Input, Metrics: in.Metrics})
if err != nil {
+18 -6
View File
@@ -61,22 +61,34 @@ func builtinSetIntersection(_ BuiltinContext, operands []*ast.Term, iter func(*a
// builtinSetUnion returns the union of the given input sets
func builtinSetUnion(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
// The set union logic here is duplicated and manually inlined on
// purpose. By lifting this logic up a level, and not doing pairwise
// set unions, we avoid a number of heap allocations. This improves
// performance dramatically over the naive approach.
result := ast.NewSet()
// The set union logic here is manually inlined on purpose. By lifting
// this logic up a level and not doing pairwise set unions, we avoid
// many heap allocations. We also pre-allocate the result set by first
// counting total elements across all input sets.
inputSet, err := builtins.SetOperand(operands[0].Value, 1)
if err != nil {
return err
}
// First pass: count total elements for pre-allocation
totalSize := 0
err = inputSet.Iter(func(x *ast.Term) error {
item, err := builtins.SetOperand(x.Value, 1)
if err != nil {
return err
}
totalSize += item.Len()
return nil
})
if err != nil {
return err
}
// Pre-allocate result set with estimated capacity
result := ast.NewSetWithCapacity(totalSize)
err = inputSet.Iter(func(x *ast.Term) error {
item, _ := builtins.SetOperand(x.Value, 1) // error checked above
item.Foreach(result.Add)
return nil
})
+4 -3
View File
@@ -614,12 +614,13 @@ func builtinTrim(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) er
return err
}
trimmed := strings.Trim(string(s), string(c))
if trimmed == string(s) {
str := string(s)
trimmed := strings.Trim(str, string(c))
if trimmed == str {
return iter(operands[0])
}
return iter(ast.InternedTerm(strings.Trim(string(s), string(c))))
return iter(ast.InternedTerm(trimmed))
}
func builtinTrimLeft(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
+3 -3
View File
@@ -2,6 +2,7 @@ package topdown
import (
"bytes"
"strings"
"text/template"
"github.com/open-policy-agent/opa/v1/ast"
@@ -30,14 +31,13 @@ func renderTemplate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
return err
}
// Do not attempt to render if template variable keys are missing
tmpl.Option("missingkey=error")
var buf bytes.Buffer
if err := tmpl.Execute(&buf, templateVariables); err != nil {
return err
}
return iter(ast.StringTerm(buf.String()))
res := strings.ReplaceAll(buf.String(), "<no value>", "<undefined>")
return iter(ast.StringTerm(res))
}
func init() {
+6
View File
@@ -2,6 +2,7 @@ package util
import (
"slices"
"strconv"
"strings"
"sync"
"unsafe"
@@ -108,6 +109,11 @@ func NumDigitsUint(n uint64) int {
return count
}
// AppendInt is a less messy version of strconv.AppendInt for base 10 ints.
func AppendInt(buf []byte, n int) []byte {
return strconv.AppendInt(buf, int64(n), 10)
}
// SplitMap calls fn for each delim-separated part of text and returns a slice of the results.
// Cheaper than calling fn on strings.Split(text, delim), as it avoids allocating an intermediate slice of strings.
func SplitMap[T any](text string, delim string, fn func(string) T) []T {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"runtime/debug"
)
var Version = "1.12.3"
var Version = "1.13.1"
// GoVersion is the version of Go this was built with
var GoVersion = runtime.Version()