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

Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.18.2 to 1.19.0.
- [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.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.19.0
  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-08-03 14:18:20 +00:00
committed by Ralf Haferkamp
parent 9df3881128
commit e12966a4fd
88 changed files with 17067 additions and 1406 deletions

6
go.mod
View File

@@ -61,7 +61,7 @@ require (
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.32.0
github.com/onsi/gomega v1.42.1
github.com/open-policy-agent/opa v1.18.2
github.com/open-policy-agent/opa v1.19.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/reva/v2 v2.47.0
@@ -210,7 +210,7 @@ require (
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-micro/plugins/v4/events/natsjs v1.2.2 // indirect
github.com/go-micro/plugins/v4/store/nats-js v1.2.1 // indirect
@@ -362,7 +362,7 @@ require (
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/vektah/gqlparser/v2 v2.5.34 // indirect
github.com/vektah/gqlparser/v2 v2.5.36 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect

20
go.sum
View File

@@ -195,8 +195,6 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3 h1:h8Z0hBv5tg/uZMKu8V47+DKWYVQg0lYP8lXDQq7uRpE=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3/go.mod h1:eE/tD53n3KbVrzrWxKLxdkGw45Fg1qaNLWjpJMvIUF4=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -281,8 +279,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjY
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
github.com/dgraph-io/badger/v4 v4.9.2 h1:Wb5qw8gElqwV1a8msHTeQKova9b1V10heFKMIiPd80E=
github.com/dgraph-io/badger/v4 v4.9.2/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/badger/v4 v4.9.4 h1:bcw+waCpzRZ2nmcSPbnPvDVhiEsn98TKmvnAhK7r7LM=
github.com/dgraph-io/badger/v4 v4.9.4/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
@@ -412,8 +410,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-micro/plugins/v4/client/grpc v1.2.1 h1:7xAwZRCO6mdUtBHsYIQs1/eCTdhCrnjF70GB+AVd6L0=
@@ -936,8 +934,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/open-policy-agent/opa v1.18.2 h1:VBiLJpioTuk7XTW1JoQi4ILo+FVxD2/8uD8iP9/OcxY=
github.com/open-policy-agent/opa v1.18.2/go.mod h1:9GY+hER4ZEXtxPlMjftVbqJJY9xLtCD3Q0oufRCfAKo=
github.com/open-policy-agent/opa v1.19.0 h1:+j2OCsjMezZEML2T1lI9giJdGJS/PL1XFKgkHPGIhpo=
github.com/open-policy-agent/opa v1.19.0/go.mod h1:pb6Y6klyf7X7X8uXNDflruA9dQC2gMqWROXI5w/kvv0=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
@@ -1189,6 +1187,8 @@ github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0 h1:a1ipjF7d/VxPX1dgVPIk4F+t6YkgMbE2OtBuRQCHJt8=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0/go.mod h1:OWSeUDiGMUy30iMsAltIJIo9uh/CleLv6KyxjYOsgR8=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
@@ -1227,8 +1227,8 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vektah/gqlparser/v2 v2.5.34 h1:MEea5P0qhdcqfBL45ghKE+qr9laidVHTMHjav5h7ckk=
github.com/vektah/gqlparser/v2 v2.5.34/go.mod h1:mFdHLGCio7OGX1fby9ZjTW6FN+qxgmbnBcRIeeScE5s=
github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s=
github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=

View File

@@ -1,5 +1,4 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.

View File

@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.

View File

@@ -426,7 +426,7 @@ func (f Formatter) colon() byte {
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0)
return f.prettyWithFlags(value, 0, 0, 0, nil)
}
const (
@@ -434,7 +434,13 @@ const (
)
// TODO: This is not fast. Most of the overhead goes here.
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// value: The value to render
// flags: Bitmask of flags (see above)
// depth: The current depth of nested structs, slices, arrays, and maps
// ptrDepth: The current depth of including pointer dereferences
// ptrMap: A map of pointers already seen, to avoid infinite recursion (usually
// nil unless ptrDepth is large)
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth int, ptrMap map[uintptr]bool) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}
@@ -504,7 +510,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -576,7 +582,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1, ptrDepth+1, ptrMap))
continue
}
if name == "" {
@@ -585,7 +591,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// field names can't contain characters which need escaping
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -612,7 +618,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
buf.WriteByte(']')
return buf.String()
@@ -637,7 +643,8 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
// key depth is unrelated to overall depth
keystr = f.prettyWithFlags(it.Key().Interface(), 0, 0, ptrDepth, ptrMap)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
@@ -646,16 +653,34 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
buf.WriteString(keystr)
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1, ptrDepth+1, ptrMap))
i++
}
buf.WriteByte('}')
return buf.String()
case reflect.Ptr, reflect.Interface:
case reflect.Pointer, reflect.Interface:
if v.IsNil() {
return "null"
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
// Special case: recursive pointers. For normal use we do not want to
// count pointer dereferences as depth, but if we see the same pointer
// again we have a recursion and need to stop. After a large number of
// pointer dereferences we will start tracking pointers to avoid the
// perf hit of doing it in the normal path.
//
// This should not happen accidentally (e.g. json decoding should never
// do this) but we can handle it gracefully.
if ptrMap != nil && ptrMap[uintptr(v.Pointer())] {
depth = f.opts.MaxLogDepth + 1 // force a depth error
}
const maxDepthFactor = 4 // arbitrary, but we want it large enough to not false-alert
if ptrDepth > f.opts.MaxLogDepth*maxDepthFactor && ptrMap == nil {
ptrMap = map[uintptr]bool{}
}
if ptrMap != nil {
ptrMap[(uintptr)(v.Pointer())] = true
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth, ptrDepth+1, ptrMap)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
@@ -697,7 +722,7 @@ func isEmpty(v reflect.Value) bool {
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Ptr:
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false

View File

@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
@@ -33,7 +32,7 @@ const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
return true
})
@@ -49,7 +48,7 @@ func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
}
l.AddValues(kvList)
return &l
@@ -61,14 +60,25 @@ func (l fnlogger) WithGroup(name string) logr.SlogSink {
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
// and other details of slog. maxDepth bounds recursion into nested groups so a
// deeply-nested slog.Group cannot exhaust the stack; it is decremented per group
// level and starts at the Formatter's MaxLogDepth (past which the formatter would
// truncate the rendering anyway).
func attrToKVs(attr slog.Attr, kvList []any, maxDepth int) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
if maxDepth <= 0 {
// Nesting is too deep to build without risking a stack overflow.
// Stop here; the formatter truncates below MaxLogDepth regardless.
if attr.Key != "" {
kvList = append(kvList, attr.Key, "<max-log-depth-exceeded>")
}
return kvList
}
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs)
grpKVs = attrToKVs(attr, grpKVs, maxDepth-1)
}
if attr.Key == "" {
// slog says we have to inline these

View File

@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.

View File

@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.

View File

@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

View File

@@ -4,7 +4,14 @@ package gojsonschema
import (
"bytes"
"sync"
"text/template"
// A method-less copy of text/template (see internal/methodlesstemplate). Locale
// format strings expand only simple {{.field}} placeholders over ErrorDetails
// (map[string]any), which has no methods, so eliding method calls is a no-op
// here; it keeps text/template's evalField MethodByName off the reachable
// graph, which otherwise disables the Go linker's method-level dead-code
// elimination binary-wide (golang/go#72895, #7903).
template "github.com/open-policy-agent/opa/internal/methodlesstemplate"
)
var errorTemplates = errorTemplate{template.New("errors-new"), sync.RWMutex{}}

View File

@@ -31,7 +31,10 @@ import (
"errors"
"math/big"
"regexp"
"text/template"
// Method-less copy of text/template; see the import note in errors.go
// (golang/go#72895, #7903). ErrorTemplateFuncs below is its FuncMap.
template "github.com/open-policy-agent/opa/internal/methodlesstemplate"
"github.com/xeipuuv/gojsonreference"
)

View File

@@ -0,0 +1,79 @@
// 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.
//go:build go1.27
package jsonv2
// This migration must not change OPA's JSON output, but json.Marshal defaults
// to v2 semantics (no HTML escaping, non-deterministic map key order). So
// every entry point into v2 here must establish v1 options (see
// [jsonv1.DefaultOptionsV1]); nested encodes inherit them from the caller's
// encoder. MarshalMarshalerTo is the only such entry point today, but that's
// incidental — any new json.Marshal, json.MarshalWrite, or jsontext.NewEncoder
// added here must do the same.
import (
jsonv1 "encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"reflect"
)
// WriteMarshalerToArray writes the JSON array of items to the encoder.
func WriteMarshalerToArray[T json.MarshalerTo](e *jsontext.Encoder, items []T) error {
e.WriteToken(jsontext.BeginArray)
for _, item := range items {
if err := item.MarshalJSONTo(e); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndArray)
}
// WriteField writes the object member name and then v's JSON encoding, so that
// the member is written and checked in one statement. A nil v is written as
// JSON null rather than dispatched to MarshalJSONTo: v1's reflection-based
// encoder already does this for a nil pointer, so writing null here keeps
// output identical to v1, rather than panicking on the types whose
// MarshalJSONTo assumes a non-nil receiver.
func WriteField[T json.MarshalerTo](e *jsontext.Encoder, name string, v T) error {
e.WriteToken(jsontext.String(name))
if rv := reflect.ValueOf(v); rv.Kind() == reflect.Pointer && rv.IsNil() {
return e.WriteToken(jsontext.Null)
}
return v.MarshalJSONTo(e)
}
// WriteFieldArray writes the object member name and then the JSON array of items.
func WriteFieldArray[T json.MarshalerTo](e *jsontext.Encoder, name string, items []T) error {
e.WriteToken(jsontext.String(name))
return WriteMarshalerToArray(e, items)
}
// WriteFieldValue is [WriteField] for values that don't implement [json.MarshalerTo].
func WriteFieldValue(e *jsontext.Encoder, name string, v any) error {
e.WriteToken(jsontext.String(name))
return json.MarshalEncode(e, v)
}
// WriteMarshalerToArrayOrNull is [WriteMarshalerToArray] but writes null for a nil
// slice, as encoding/json v1 does. Types whose pre-1.27 MarshalJSON returns "[]"
// for an empty value must keep using [WriteMarshalerToArray].
func WriteMarshalerToArrayOrNull[T json.MarshalerTo](e *jsontext.Encoder, items []T) error {
if items == nil {
return e.WriteToken(jsontext.Null)
}
return WriteMarshalerToArray(e, items)
}
// MarshalMarshalerTo provides a MarshalJSON implementation for any type that
// implements json.MarshalerTo. json.Marshal dispatches to MarshalJSONTo, so this
// doesn't recurse; the constraint is what guarantees that at compile time.
//
// This is the entry point into v2 that establishes v1 options, per the
// package-level comment above.
func MarshalMarshalerTo[T json.MarshalerTo](v T) ([]byte, error) {
return json.Marshal(v, jsonv1.DefaultOptionsV1())
}

View File

@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,28 @@
# methodlesstemplate
A vendored copy of the Go standard library's `text/template`, with **one** behavioral change: the
method-calls-on-data branch in `exec.go`'s `evalField` (`reflect.Value.MethodByName`) is removed.
A reachable, non-constant `reflect.Value.MethodByName` makes the Go linker disable method-level
dead-code elimination for the **entire binary** (golang/go#72895). OPA reaches `text/template` from
its compiler (schema errors) and the `strings.render_template` builtin, so that one edge retains the
full reflected method surface of every embedder — a large binary-size regression (#7903). Rego
values and gojsonschema `ErrorDetails` decode to `map[string]any` / `[]any` / scalars, which have no
methods, so eliding the data-method lookup is a behavioral no-op while restoring DCE.
Only `exec.go` differs from the upstream stdlib. `doc.go`, `funcs.go`, `option.go`, `template.go`,
and `internal/fmtsort/sort.go` are byte-identical to their Go release; `text/template/parse` is
reused via its normal import. `helper.go` (ParseFiles/ParseGlob/ParseFS) is intentionally not
vendored. Go's BSD `LICENSE` and per-file copyright headers are preserved.
## Regenerating
Do not hand-edit these files. To re-sync to a new Go release, run under the target toolchain:
```
GOTOOLCHAIN=go1.26.0 build/regen-methodless-template.sh
```
The script copies the stdlib files verbatim and re-applies the single method-elision edit. If the
edit no longer applies (the stdlib changed that region), the script fails and the elision must be
re-derived and the patch in the script updated.

View File

@@ -0,0 +1,502 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package template implements data-driven templates for generating textual output.
To generate HTML output, see [html/template], which has the same interface
as this package but automatically secures HTML output against certain attacks.
Templates are executed by applying them to a data structure. Annotations in the
template refer to elements of the data structure (typically a field of a struct
or a key in a map) to control execution and derive values to be displayed.
Execution of the template walks the structure and sets the cursor, represented
by a period '.' and called "dot", to the value at the current location in the
structure as execution proceeds.
The security model used by this package assumes that template authors are
trusted. The package does not auto-escape output, so injecting code into
a template can lead to arbitrary code execution if the template is executed
by an untrusted source.
The input text for a template is UTF-8-encoded text in any format.
"Actions"--data evaluations or control structures--are delimited by
"{{" and "}}"; all text outside actions is copied to the output unchanged.
Once parsed, a template may be executed safely in parallel, although if parallel
executions share a Writer the output may be interleaved.
Here is a trivial example that prints "17 items are made of wool".
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"wool", 17}
tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")
if err != nil { panic(err) }
err = tmpl.Execute(os.Stdout, sweaters)
if err != nil { panic(err) }
More intricate examples appear below.
Text and spaces
By default, all text between actions is copied verbatim when the template is
executed. For example, the string " items are made of " in the example above
appears on standard output when the program is run.
However, to aid in formatting template source code, if an action's left
delimiter (by default "{{") is followed immediately by a minus sign and white
space, all trailing white space is trimmed from the immediately preceding text.
Similarly, if the right delimiter ("}}") is preceded by white space and a minus
sign, all leading white space is trimmed from the immediately following text.
In these trim markers, the white space must be present:
"{{- 3}}" is like "{{3}}" but trims the immediately preceding text, while
"{{-3}}" parses as an action containing the number -3.
For instance, when executing the template whose source is
"{{23 -}} < {{- 45}}"
the generated output would be
"23<45"
For this trimming, the definition of white space characters is the same as in Go:
space, horizontal tab, carriage return, and newline.
Actions
Here is the list of actions. "Arguments" and "pipelines" are evaluations of
data, defined in detail in the corresponding sections that follow.
*/
// {{/* a comment */}}
// {{- /* a comment with white space trimmed from preceding and following text */ -}}
// A comment; discarded. May contain newlines.
// Comments do not nest and must start and end at the
// delimiters, as shown here.
/*
{{pipeline}}
The default textual representation (the same as would be
printed by fmt.Print) of the value of the pipeline is copied
to the output.
{{if pipeline}} T1 {{end}}
If the value of the pipeline is empty, no output is generated;
otherwise, T1 is executed. The empty values are false, 0, any
nil pointer or interface value, and any array, slice, map, or
string of length zero.
Dot is unaffected.
{{if pipeline}} T1 {{else}} T0 {{end}}
If the value of the pipeline is empty, T0 is executed;
otherwise, T1 is executed. Dot is unaffected.
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
To simplify the appearance of if-else chains, the else action
of an if may include another if directly; the effect is exactly
the same as writing
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
{{range pipeline}} T1 {{end}}
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2, integer or channel.
If the value of the pipeline has length zero, nothing is output;
otherwise, dot is set to the successive elements of the array,
slice, or map and T1 is executed. If the value is a map and the
keys are of basic type with a defined order, the elements will be
visited in sorted key order.
{{range pipeline}} T1 {{else}} T0 {{end}}
The value of the pipeline must be an array, slice, map, iter.Seq,
iter.Seq2, integer or channel.
If the value of the pipeline has length zero, dot is unaffected and
T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed.
{{break}}
The innermost {{range pipeline}} loop is ended early, stopping the
current iteration and bypassing all remaining iterations.
{{continue}}
The current iteration of the innermost {{range pipeline}} loop is
stopped, and the loop starts the next iteration.
{{template "name"}}
The template with the specified name is executed with nil data.
{{template "name" pipeline}}
The template with the specified name is executed with dot set
to the value of the pipeline.
{{block "name" pipeline}} T1 {{end}}
A block is shorthand for defining a template
{{define "name"}} T1 {{end}}
and then executing it in place
{{template "name" pipeline}}
The typical use is to define a set of root templates that are
then customized by redefining the block templates within.
{{with pipeline}} T1 {{end}}
If the value of the pipeline is empty, no output is generated;
otherwise, dot is set to the value of the pipeline and T1 is
executed.
{{with pipeline}} T1 {{else}} T0 {{end}}
If the value of the pipeline is empty, dot is unaffected and T0
is executed; otherwise, dot is set to the value of the pipeline
and T1 is executed.
{{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
To simplify the appearance of with-else chains, the else action
of a with may include another with directly; the effect is exactly
the same as writing
{{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
Arguments
An argument is a simple value, denoted by one of the following.
- A boolean, string, character, integer, floating-point, imaginary
or complex constant in Go syntax. These behave like Go's untyped
constants. Note that, as in Go, whether a large integer constant
overflows when assigned or passed to a function can depend on whether
the host machine's ints are 32 or 64 bits.
- The keyword nil, representing an untyped Go nil.
- The character '.' (period):
.
The result is the value of dot.
- A variable name, which is a (possibly empty) alphanumeric string
preceded by a dollar sign, such as
$piOver2
or
$
The result is the value of the variable.
Variables are described below.
- The name of a field of the data, which must be a struct, preceded
by a period, such as
.Field
The result is the value of the field. Field invocations may be
chained:
.Field1.Field2
Fields can also be evaluated on variables, including chaining:
$x.Field1.Field2
- The name of a key of the data, which must be a map, preceded
by a period, such as
.Key
The result is the map element value indexed by the key.
Key invocations may be chained and combined with fields to any
depth:
.Field1.Key1.Field2.Key2
Although the key must be an alphanumeric identifier, unlike with
field names they do not need to start with an upper case letter.
Keys can also be evaluated on variables, including chaining:
$x.key1.key2
- The name of a niladic method of the data, preceded by a period,
such as
.Method
The result is the value of invoking the method with dot as the
receiver, dot.Method(). Such a method must have one return value (of
any type) or two return values, the second of which is an error.
If it has two and the returned error is non-nil, execution terminates
and an error is returned to the caller as the value of Execute.
Method invocations may be chained and combined with fields and keys
to any depth:
.Field1.Key1.Method1.Field2.Key2.Method2
Methods can also be evaluated on variables, including chaining:
$x.Method1.Field
- The name of a niladic function, such as
fun
The result is the value of invoking the function, fun(). The return
types and values behave as in methods. Functions and function
names are described below.
- A parenthesized instance of one the above, for grouping. The result
may be accessed by a field or map key invocation.
print (.F1 arg1) (.F2 arg2)
(.StructValuedMethod "arg").Field
Arguments may evaluate to any type; if they are pointers the implementation
automatically indirects to the base type when required.
If an evaluation yields a function value, such as a function-valued
field of a struct, the function is not invoked automatically, but it
can be used as a truth value for an if action and the like. To invoke
it, use the call function, defined below.
Pipelines
A pipeline is a possibly chained sequence of "commands". A command is a simple
value (argument) or a function or method call, possibly with multiple arguments:
Argument
The result is the value of evaluating the argument.
.Method [Argument...]
The method can be alone or the last element of a chain but,
unlike methods in the middle of a chain, it can take arguments.
The result is the value of calling the method with the
arguments:
dot.Method(Argument1, etc.)
functionName [Argument...]
The result is the value of calling the function associated
with the name:
function(Argument1, etc.)
Functions and function names are described below.
A pipeline may be "chained" by separating a sequence of commands with pipeline
characters '|'. In a chained pipeline, the result of each command is
passed as the last argument of the following command. The output of the final
command in the pipeline is the value of the pipeline.
The output of a command will be either one value or two values, the second of
which has type error. If that second value is present and evaluates to
non-nil, execution terminates and the error is returned to the caller of
Execute.
Variables
A pipeline inside an action may initialize a variable to capture the result.
The initialization has syntax
$variable := pipeline
where $variable is the name of the variable. An action that declares a
variable produces no output.
Variables previously declared can also be assigned, using the syntax
$variable = pipeline
If a "range" action initializes a variable, the variable is set to the
successive elements of the iteration. Also, a "range" may declare two
variables, separated by a comma:
range $index, $element := pipeline
in which case $index and $element are set to the successive values of the
array/slice index or map key and element, respectively. Note that if there is
only one variable, it is assigned the element; this is opposite to the
convention in Go range clauses.
A variable's scope extends to the "end" action of the control structure ("if",
"with", or "range") in which it is declared, or to the end of the template if
there is no such control structure. A template invocation does not inherit
variables from the point of its invocation.
When execution begins, $ is set to the data argument passed to Execute, that is,
to the starting value of dot.
Examples
Here are some example one-line templates demonstrating pipelines and variables.
All produce the quoted word "output":
{{"\"output\""}}
A string constant.
{{`"output"`}}
A raw string constant.
{{printf "%q" "output"}}
A function call.
{{"output" | printf "%q"}}
A function call whose final argument comes from the previous
command.
{{printf "%q" (print "out" "put")}}
A parenthesized argument.
{{"put" | printf "%s%s" "out" | printf "%q"}}
A more elaborate call.
{{"output" | printf "%s" | printf "%q"}}
A longer chain.
{{with "output"}}{{printf "%q" .}}{{end}}
A with action using dot.
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
A with action that creates and uses a variable.
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
A with action that uses the variable in another action.
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
The same, but pipelined.
Functions
During execution functions are found in two function maps: first in the
template, then in the global function map. By default, no functions are defined
in the template but the Funcs method can be used to add them.
Predefined global functions are named as follows.
and
Returns the boolean AND of its arguments by returning the
first empty argument or the last argument. That is,
"and x y" behaves as "if x then y else x."
Evaluation proceeds through the arguments left to right
and returns when the result is determined.
call
Returns the result of calling the first argument, which
must be a function, with the remaining arguments as parameters.
Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where
Y is a func-valued field, map entry, or the like.
The first argument must be the result of an evaluation
that yields a value of function type (as distinct from
a predefined function such as print). The function must
return either one or two result values, the second of which
is of type error. If the arguments don't match the function
or the returned error value is non-nil, execution stops.
html
Returns the escaped HTML equivalent of the textual
representation of its arguments. This function is unavailable
in html/template, with a few exceptions.
index
Returns the result of indexing its first argument by the
following arguments. Thus "index x 1 2 3" is, in Go syntax,
x[1][2][3]. Each indexed item must be a map, slice, or array.
slice
slice returns the result of slicing its first argument by the
remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2],
while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3"
is x[1:2:3]. The first argument must be a string, slice, or array.
js
Returns the escaped JavaScript equivalent of the textual
representation of its arguments.
len
Returns the integer length of its argument.
not
Returns the boolean negation of its single argument.
or
Returns the boolean OR of its arguments by returning the
first non-empty argument or the last argument, that is,
"or x y" behaves as "if x then x else y".
Evaluation proceeds through the arguments left to right
and returns when the result is determined.
print
An alias for fmt.Sprint
printf
An alias for fmt.Sprintf
println
An alias for fmt.Sprintln
urlquery
Returns the escaped value of the textual representation of
its arguments in a form suitable for embedding in a URL query.
This function is unavailable in html/template, with a few
exceptions.
The boolean functions take any zero value to be false and a non-zero
value to be true.
There is also a set of binary comparison operators defined as
functions:
eq
Returns the boolean truth of arg1 == arg2
ne
Returns the boolean truth of arg1 != arg2
lt
Returns the boolean truth of arg1 < arg2
le
Returns the boolean truth of arg1 <= arg2
gt
Returns the boolean truth of arg1 > arg2
ge
Returns the boolean truth of arg1 >= arg2
For simpler multi-way equality tests, eq (only) accepts two or more
arguments and compares the second and subsequent to the first,
returning in effect
arg1==arg2 || arg1==arg3 || arg1==arg4 ...
(Unlike with || in Go, however, eq is a function call and all the
arguments will be evaluated.)
The comparison functions work on any values whose type Go defines as
comparable. For basic types such as integers, the rules are relaxed:
size and exact type are ignored, so any integer value, signed or unsigned,
may be compared with any other integer value. (The arithmetic value is compared,
not the bit pattern, so all negative integers are less than all unsigned integers.)
However, as usual, one may not compare an int with a float32 and so on.
Associated templates
Each template is named by a string specified when it is created. Also, each
template is associated with zero or more other templates that it may invoke by
name; such associations are transitive and form a name space of templates.
A template may use a template invocation to instantiate another associated
template; see the explanation of the "template" action above. The name must be
that of a template associated with the template that contains the invocation.
Nested template definitions
When parsing a template, another template may be defined and associated with the
template being parsed. Template definitions must appear at the top level of the
template, much like global variables in a Go program.
The syntax of such definitions is to surround each template declaration with a
"define" and "end" action.
The define action names the template being created by providing a string
constant. Here is a simple example:
{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}
This defines two templates, T1 and T2, and a third T3 that invokes the other two
when it is executed. Finally it invokes T3. If executed this template will
produce the text
ONE TWO
By construction, a template may reside in only one association. If it's
necessary to have a template addressable from multiple associations, the
template definition must be parsed multiple times to create distinct *Template
values, or must be copied with [Template.Clone] or [Template.AddParseTree].
Parse may be called multiple times to assemble the various associated templates;
see [ParseFiles], [ParseGlob], [Template.ParseFiles] and [Template.ParseGlob]
for simple ways to parse related templates stored in files.
A template may be executed directly or through [Template.ExecuteTemplate], which executes
an associated template identified by name. To invoke our example above, we
might write,
err := tmpl.Execute(os.Stdout, "no data needed")
if err != nil {
log.Fatalf("execution failed: %s", err)
}
or to invoke a particular template explicitly by name,
err := tmpl.ExecuteTemplate(os.Stdout, "T2", "no data needed")
if err != nil {
log.Fatalf("execution failed: %s", err)
}
*/
package template

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,783 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package template
import (
"errors"
"fmt"
"io"
"net/url"
"reflect"
"strings"
"sync"
"unicode"
"unicode/utf8"
)
// FuncMap is the type of the map defining the mapping from names to functions.
// Each function must have either a single return value, or two return values of
// which the second has type error. In that case, if the second (error)
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
// Errors returned by Execute wrap the underlying error; call [errors.As] to
// unwrap them.
//
// When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to
// apply to arguments of arbitrary type can use parameters of type interface{} or
// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or [reflect.Value].
type FuncMap map[string]any
// builtins returns the FuncMap.
// It is not a global variable so the linker can dead code eliminate
// more when this isn't called. See golang.org/issue/36021.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtins() FuncMap {
return FuncMap{
"and": and,
"call": emptyCall,
"html": HTMLEscaper,
"index": index,
"slice": slice,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
// Comparisons
"eq": eq, // ==
"ge": ge, // >=
"gt": gt, // >
"le": le, // <=
"lt": lt, // <
"ne": ne, // !=
}
}
var builtinFuncsOnce struct {
sync.Once
v map[string]reflect.Value
}
// builtinFuncsOnce lazily computes & caches the builtinFuncs map.
// TODO: revert this back to a global map once golang.org/issue/2559 is fixed.
func builtinFuncs() map[string]reflect.Value {
builtinFuncsOnce.Do(func() {
builtinFuncsOnce.v = createValueFuncs(builtins())
})
return builtinFuncsOnce.v
}
// createValueFuncs turns a FuncMap into a map[string]reflect.Value
func createValueFuncs(funcMap FuncMap) map[string]reflect.Value {
m := make(map[string]reflect.Value)
addValueFuncs(m, funcMap)
return m
}
// addValueFuncs adds to values the functions in funcs, converting them to reflect.Values.
func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
for name, fn := range in {
if !goodName(name) {
panic(fmt.Errorf("function name %q is not a valid identifier", name))
}
v := reflect.ValueOf(fn)
if v.Kind() != reflect.Func {
panic("value for " + name + " not a function")
}
if err := goodFunc(name, v.Type()); err != nil {
panic(err)
}
out[name] = v
}
}
// addFuncs adds to values the functions in funcs. It does no checking of the input -
// call addValueFuncs first.
func addFuncs(out, in FuncMap) {
for name, fn := range in {
out[name] = fn
}
}
// goodFunc reports whether the function or method has the right result signature.
func goodFunc(name string, typ reflect.Type) error {
// We allow functions with 1 result or 2 results where the second is an error.
switch numOut := typ.NumOut(); {
case numOut == 1:
return nil
case numOut == 2 && typ.Out(1) == errorType:
return nil
case numOut == 2:
return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
default:
return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
}
}
// goodName reports whether the function name is a valid identifier.
func goodName(name string) bool {
if name == "" {
return false
}
for i, r := range name {
switch {
case r == '_':
case i == 0 && !unicode.IsLetter(r):
return false
case !unicode.IsLetter(r) && !unicode.IsDigit(r):
return false
}
}
return true
}
// findFunction looks for a function in the template, and global map.
func findFunction(name string, tmpl *Template) (v reflect.Value, isBuiltin, ok bool) {
if tmpl != nil && tmpl.common != nil {
tmpl.muFuncs.RLock()
defer tmpl.muFuncs.RUnlock()
if fn := tmpl.execFuncs[name]; fn.IsValid() {
return fn, false, true
}
}
if fn := builtinFuncs()[name]; fn.IsValid() {
return fn, true, true
}
return reflect.Value{}, false, false
}
// prepareArg checks if value can be used as an argument of type argType, and
// converts an invalid value to appropriate zero if possible.
func prepareArg(value reflect.Value, argType reflect.Type) (reflect.Value, error) {
if !value.IsValid() {
if !canBeNil(argType) {
return reflect.Value{}, fmt.Errorf("value is nil; should be of type %s", argType)
}
value = reflect.Zero(argType)
}
if value.Type().AssignableTo(argType) {
return value, nil
}
if intLike(value.Kind()) && intLike(argType.Kind()) && value.Type().ConvertibleTo(argType) {
value = value.Convert(argType)
return value, nil
}
return reflect.Value{}, fmt.Errorf("value has type %s; should be %s", value.Type(), argType)
}
func intLike(typ reflect.Kind) bool {
switch typ {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return true
}
return false
}
// indexArg checks if a reflect.Value can be used as an index, and converts it to int if possible.
func indexArg(index reflect.Value, cap int) (int, error) {
var x int64
switch index.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x = index.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
x = int64(index.Uint())
case reflect.Invalid:
return 0, fmt.Errorf("cannot index slice/array with nil")
default:
return 0, fmt.Errorf("cannot index slice/array with type %s", index.Type())
}
if x < 0 || int(x) < 0 || int(x) > cap {
return 0, fmt.Errorf("index out of range: %d", x)
}
return int(x), nil
}
// Indexing.
// index returns the result of indexing its first argument by the following
// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
// indexed item must be a map, slice, or array.
func index(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
item = indirectInterface(item)
if !item.IsValid() {
return reflect.Value{}, fmt.Errorf("index of untyped nil")
}
for _, index := range indexes {
index = indirectInterface(index)
var isNil bool
if item, isNil = indirect(item); isNil {
return reflect.Value{}, fmt.Errorf("index of nil pointer")
}
switch item.Kind() {
case reflect.Array, reflect.Slice, reflect.String:
x, err := indexArg(index, item.Len())
if err != nil {
return reflect.Value{}, err
}
item = item.Index(x)
case reflect.Map:
index, err := prepareArg(index, item.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := item.MapIndex(index); x.IsValid() {
item = x
} else {
item = reflect.Zero(item.Type().Elem())
}
case reflect.Invalid:
// the loop holds invariant: item.IsValid()
panic("unreachable")
default:
return reflect.Value{}, fmt.Errorf("can't index item of type %s", item.Type())
}
}
return item, nil
}
// Slicing.
// slice returns the result of slicing its first argument by the remaining
// arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2], while "slice x"
// is x[:], "slice x 1" is x[1:], and "slice x 1 2 3" is x[1:2:3]. The first
// argument must be a string, slice, or array.
func slice(item reflect.Value, indexes ...reflect.Value) (reflect.Value, error) {
item = indirectInterface(item)
if !item.IsValid() {
return reflect.Value{}, fmt.Errorf("slice of untyped nil")
}
if len(indexes) > 3 {
return reflect.Value{}, fmt.Errorf("too many slice indexes: %d", len(indexes))
}
var cap int
switch item.Kind() {
case reflect.String:
if len(indexes) == 3 {
return reflect.Value{}, fmt.Errorf("cannot 3-index slice a string")
}
cap = item.Len()
case reflect.Array, reflect.Slice:
cap = item.Cap()
default:
return reflect.Value{}, fmt.Errorf("can't slice item of type %s", item.Type())
}
// set default values for cases item[:], item[i:].
idx := [3]int{0, item.Len()}
for i, index := range indexes {
x, err := indexArg(index, cap)
if err != nil {
return reflect.Value{}, err
}
idx[i] = x
}
// given item[i:j], make sure i <= j.
if idx[0] > idx[1] {
return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[0], idx[1])
}
if len(indexes) < 3 {
return item.Slice(idx[0], idx[1]), nil
}
// given item[i:j:k], make sure i <= j <= k.
if idx[1] > idx[2] {
return reflect.Value{}, fmt.Errorf("invalid slice index: %d > %d", idx[1], idx[2])
}
return item.Slice3(idx[0], idx[1], idx[2]), nil
}
// Length
// length returns the length of the item, with an error if it has no defined length.
func length(item reflect.Value) (int, error) {
item, isNil := indirect(item)
if isNil {
return 0, fmt.Errorf("len of nil pointer")
}
switch item.Kind() {
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String:
return item.Len(), nil
}
return 0, fmt.Errorf("len of type %s", item.Type())
}
// Function invocation
func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
panic("unreachable") // implemented as a special case in evalCall
}
// call returns the result of evaluating the first argument as a function.
// The function must return 1 result, or 2 results, the second of which is an error.
func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
fn = indirectInterface(fn)
if !fn.IsValid() {
return reflect.Value{}, fmt.Errorf("call of nil")
}
typ := fn.Type()
if typ.Kind() != reflect.Func {
return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
}
if err := goodFunc(name, typ); err != nil {
return reflect.Value{}, err
}
numIn := typ.NumIn()
var dddType reflect.Type
if typ.IsVariadic() {
if len(args) < numIn-1 {
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1)
}
dddType = typ.In(numIn - 1).Elem()
} else {
if len(args) != numIn {
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
}
}
argv := make([]reflect.Value, len(args))
for i, arg := range args {
arg = indirectInterface(arg)
// Compute the expected type. Clumsy because of variadics.
argType := dddType
if !typ.IsVariadic() || i < numIn-1 {
argType = typ.In(i)
}
var err error
if argv[i], err = prepareArg(arg, argType); err != nil {
return reflect.Value{}, fmt.Errorf("arg %d: %w", i, err)
}
}
return safeCall(fn, argv)
}
// safeCall runs fun.Call(args), and returns the resulting value and error, if
// any. If the call panics, the panic value is returned as an error.
func safeCall(fun reflect.Value, args []reflect.Value) (val reflect.Value, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("%v", r)
}
}
}()
ret := fun.Call(args)
if len(ret) == 2 && !ret[1].IsNil() {
return ret[0], ret[1].Interface().(error)
}
return ret[0], nil
}
// Boolean logic.
func truth(arg reflect.Value) bool {
t, _ := isTrue(indirectInterface(arg))
return t
}
// and computes the Boolean AND of its arguments, returning
// the first false argument it encounters, or the last argument.
func and(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
panic("unreachable") // implemented as a special case in evalCall
}
// or computes the Boolean OR of its arguments, returning
// the first true argument it encounters, or the last argument.
func or(arg0 reflect.Value, args ...reflect.Value) reflect.Value {
panic("unreachable") // implemented as a special case in evalCall
}
// not returns the Boolean negation of its argument.
func not(arg reflect.Value) bool {
return !truth(arg)
}
// Comparison.
// TODO: Perhaps allow comparison between signed and unsigned integers.
var (
errBadComparisonType = errors.New("invalid type for comparison")
errNoComparison = errors.New("missing argument for comparison")
)
type kind int
const (
invalidKind kind = iota
boolKind
complexKind
intKind
floatKind
stringKind
uintKind
)
func basicKind(v reflect.Value) (kind, error) {
switch v.Kind() {
case reflect.Bool:
return boolKind, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intKind, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintKind, nil
case reflect.Float32, reflect.Float64:
return floatKind, nil
case reflect.Complex64, reflect.Complex128:
return complexKind, nil
case reflect.String:
return stringKind, nil
}
return invalidKind, errBadComparisonType
}
// isNil returns true if v is the zero reflect.Value, or nil of its type.
func isNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
}
return false
}
// canCompare reports whether v1 and v2 are both the same kind, or one is nil.
// Called only when dealing with nillable types, or there's about to be an error.
func canCompare(v1, v2 reflect.Value) bool {
k1 := v1.Kind()
k2 := v2.Kind()
if k1 == k2 {
return true
}
// We know the type can be compared to nil.
return k1 == reflect.Invalid || k2 == reflect.Invalid
}
// eq evaluates the comparison a == b || a == c || ...
func eq(arg1 reflect.Value, arg2 ...reflect.Value) (bool, error) {
arg1 = indirectInterface(arg1)
if len(arg2) == 0 {
return false, errNoComparison
}
k1, _ := basicKind(arg1)
for _, arg := range arg2 {
arg = indirectInterface(arg)
k2, _ := basicKind(arg)
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = arg1.Int() >= 0 && uint64(arg1.Int()) == arg.Uint()
case k1 == uintKind && k2 == intKind:
truth = arg.Int() >= 0 && arg1.Uint() == uint64(arg.Int())
default:
if arg1.IsValid() && arg.IsValid() {
return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg.Type())
}
}
} else {
switch k1 {
case boolKind:
truth = arg1.Bool() == arg.Bool()
case complexKind:
truth = arg1.Complex() == arg.Complex()
case floatKind:
truth = arg1.Float() == arg.Float()
case intKind:
truth = arg1.Int() == arg.Int()
case stringKind:
truth = arg1.String() == arg.String()
case uintKind:
truth = arg1.Uint() == arg.Uint()
default:
if !canCompare(arg1, arg) {
return false, fmt.Errorf("non-comparable types %s: %v, %s: %v", arg1, arg1.Type(), arg.Type(), arg)
}
if isNil(arg1) || isNil(arg) {
truth = isNil(arg) == isNil(arg1)
} else {
if !arg.Type().Comparable() {
return false, fmt.Errorf("non-comparable type %s: %v", arg, arg.Type())
}
truth = arg1.Interface() == arg.Interface()
}
}
}
if truth {
return true, nil
}
}
return false, nil
}
// ne evaluates the comparison a != b.
func ne(arg1, arg2 reflect.Value) (bool, error) {
// != is the inverse of ==.
equal, err := eq(arg1, arg2)
return !equal, err
}
// lt evaluates the comparison a < b.
func lt(arg1, arg2 reflect.Value) (bool, error) {
arg1 = indirectInterface(arg1)
k1, err := basicKind(arg1)
if err != nil {
return false, err
}
arg2 = indirectInterface(arg2)
k2, err := basicKind(arg2)
if err != nil {
return false, err
}
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = arg1.Int() < 0 || uint64(arg1.Int()) < arg2.Uint()
case k1 == uintKind && k2 == intKind:
truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
default:
return false, fmt.Errorf("incompatible types for comparison: %v and %v", arg1.Type(), arg2.Type())
}
} else {
switch k1 {
case boolKind, complexKind:
return false, errBadComparisonType
case floatKind:
truth = arg1.Float() < arg2.Float()
case intKind:
truth = arg1.Int() < arg2.Int()
case stringKind:
truth = arg1.String() < arg2.String()
case uintKind:
truth = arg1.Uint() < arg2.Uint()
default:
panic("invalid kind")
}
}
return truth, nil
}
// le evaluates the comparison <= b.
func le(arg1, arg2 reflect.Value) (bool, error) {
// <= is < or ==.
lessThan, err := lt(arg1, arg2)
if lessThan || err != nil {
return lessThan, err
}
return eq(arg1, arg2)
}
// gt evaluates the comparison a > b.
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
lessOrEqual, err := le(arg1, arg2)
if err != nil {
return false, err
}
return !lessOrEqual, nil
}
// ge evaluates the comparison a >= b.
func ge(arg1, arg2 reflect.Value) (bool, error) {
// >= is the inverse of <.
lessThan, err := lt(arg1, arg2)
if err != nil {
return false, err
}
return !lessThan, nil
}
// HTML escaping.
var (
htmlQuot = []byte("&#34;") // shorter than "&quot;"
htmlApos = []byte("&#39;") // shorter than "&apos;" and apos was not in HTML until HTML5
htmlAmp = []byte("&amp;")
htmlLt = []byte("&lt;")
htmlGt = []byte("&gt;")
htmlNull = []byte("\uFFFD")
)
// HTMLEscape writes to w the escaped HTML equivalent of the plain text data b.
func HTMLEscape(w io.Writer, b []byte) {
last := 0
for i, c := range b {
var html []byte
switch c {
case '\000':
html = htmlNull
case '"':
html = htmlQuot
case '\'':
html = htmlApos
case '&':
html = htmlAmp
case '<':
html = htmlLt
case '>':
html = htmlGt
default:
continue
}
w.Write(b[last:i])
w.Write(html)
last = i + 1
}
w.Write(b[last:])
}
// HTMLEscapeString returns the escaped HTML equivalent of the plain text data s.
func HTMLEscapeString(s string) string {
// Avoid allocation if we can.
if !strings.ContainsAny(s, "'\"&<>\000") {
return s
}
var b strings.Builder
HTMLEscape(&b, []byte(s))
return b.String()
}
// HTMLEscaper returns the escaped HTML equivalent of the textual
// representation of its arguments.
func HTMLEscaper(args ...any) string {
return HTMLEscapeString(evalArgs(args))
}
// JavaScript escaping.
var (
jsLowUni = []byte(`\u00`)
hex = []byte("0123456789ABCDEF")
jsBackslash = []byte(`\\`)
jsApos = []byte(`\'`)
jsQuot = []byte(`\"`)
jsLt = []byte(`\u003C`)
jsGt = []byte(`\u003E`)
jsAmp = []byte(`\u0026`)
jsEq = []byte(`\u003D`)
)
// JSEscape writes to w the escaped JavaScript equivalent of the plain text data b.
func JSEscape(w io.Writer, b []byte) {
last := 0
for i := 0; i < len(b); i++ {
c := b[i]
if !jsIsSpecial(rune(c)) {
// fast path: nothing to do
continue
}
w.Write(b[last:i])
if c < utf8.RuneSelf {
// Quotes, slashes and angle brackets get quoted.
// Control characters get written as \u00XX.
switch c {
case '\\':
w.Write(jsBackslash)
case '\'':
w.Write(jsApos)
case '"':
w.Write(jsQuot)
case '<':
w.Write(jsLt)
case '>':
w.Write(jsGt)
case '&':
w.Write(jsAmp)
case '=':
w.Write(jsEq)
default:
w.Write(jsLowUni)
t, b := c>>4, c&0x0f
w.Write(hex[t : t+1])
w.Write(hex[b : b+1])
}
} else {
// Unicode rune.
r, size := utf8.DecodeRune(b[i:])
if unicode.IsPrint(r) {
w.Write(b[i : i+size])
} else {
fmt.Fprintf(w, "\\u%04X", r)
}
i += size - 1
}
last = i + 1
}
w.Write(b[last:])
}
// JSEscapeString returns the escaped JavaScript equivalent of the plain text data s.
func JSEscapeString(s string) string {
// Avoid allocation if we can.
if strings.IndexFunc(s, jsIsSpecial) < 0 {
return s
}
var b strings.Builder
JSEscape(&b, []byte(s))
return b.String()
}
func jsIsSpecial(r rune) bool {
switch r {
case '\\', '\'', '"', '<', '>', '&', '=':
return true
}
return r < ' ' || utf8.RuneSelf <= r
}
// JSEscaper returns the escaped JavaScript equivalent of the textual
// representation of its arguments.
func JSEscaper(args ...any) string {
return JSEscapeString(evalArgs(args))
}
// URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query.
func URLQueryEscaper(args ...any) string {
return url.QueryEscape(evalArgs(args))
}
// evalArgs formats the list of arguments into a string. It is therefore equivalent to
//
// fmt.Sprint(args...)
//
// except that each argument is indirected (if a pointer), as required,
// using the same rules as the default string evaluation during template
// execution.
func evalArgs(args []any) string {
ok := false
var s string
// Fast path for simple common case.
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
for i, arg := range args {
a, ok := printableValue(reflect.ValueOf(arg))
if ok {
args[i] = a
} // else let fmt do its thing
}
s = fmt.Sprint(args...)
}
return s
}

View File

@@ -0,0 +1,154 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package fmtsort provides a general stable ordering mechanism
// for maps, on behalf of the fmt and text/template packages.
// It is not guaranteed to be efficient and works only for types
// that are valid map keys.
package fmtsort
import (
"cmp"
"reflect"
"slices"
)
// Note: Throughout this package we avoid calling reflect.Value.Interface as
// it is not always legal to do so and it's easier to avoid the issue than to face it.
// SortedMap is a slice of KeyValue pairs that simplifies sorting
// and iterating over map entries.
//
// Each KeyValue pair contains a map key and its corresponding value.
type SortedMap []KeyValue
// KeyValue holds a single key and value pair found in a map.
type KeyValue struct {
Key, Value reflect.Value
}
// Sort accepts a map and returns a SortedMap that has the same keys and
// values but in a stable sorted order according to the keys, modulo issues
// raised by unorderable key values such as NaNs.
//
// The ordering rules are more general than with Go's < operator:
//
// - when applicable, nil compares low
// - ints, floats, and strings order by <
// - NaN compares less than non-NaN floats
// - bool compares false before true
// - complex compares real, then imag
// - pointers compare by machine address
// - channel values compare by machine address
// - structs compare each field in turn
// - arrays compare each element in turn.
// Otherwise identical arrays compare by length.
// - interface values compare first by reflect.Type describing the concrete type
// and then by concrete value as described in the previous rules.
func Sort(mapValue reflect.Value) SortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
}
// Note: this code is arranged to not panic even in the presence
// of a concurrent map update. The runtime is responsible for
// yelling loudly if that happens. See issue 33275.
n := mapValue.Len()
sorted := make(SortedMap, 0, n)
iter := mapValue.MapRange()
for iter.Next() {
sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
}
slices.SortStableFunc(sorted, func(a, b KeyValue) int {
return compare(a.Key, b.Key)
})
return sorted
}
// compare compares two values of the same type. It returns -1, 0, 1
// according to whether a > b (1), a == b (0), or a < b (-1).
// If the types differ, it returns -1.
// See the comment on Sort for the comparison rules.
func compare(aVal, bVal reflect.Value) int {
aType, bType := aVal.Type(), bVal.Type()
if aType != bType {
return -1 // No good answer possible, but don't return 0: they're not equal.
}
switch aVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return cmp.Compare(aVal.Int(), bVal.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return cmp.Compare(aVal.Uint(), bVal.Uint())
case reflect.String:
return cmp.Compare(aVal.String(), bVal.String())
case reflect.Float32, reflect.Float64:
return cmp.Compare(aVal.Float(), bVal.Float())
case reflect.Complex64, reflect.Complex128:
a, b := aVal.Complex(), bVal.Complex()
if c := cmp.Compare(real(a), real(b)); c != 0 {
return c
}
return cmp.Compare(imag(a), imag(b))
case reflect.Bool:
a, b := aVal.Bool(), bVal.Bool()
switch {
case a == b:
return 0
case a:
return 1
default:
return -1
}
case reflect.Pointer, reflect.UnsafePointer:
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Chan:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
return c
}
}
return 0
case reflect.Array:
for i := 0; i < aVal.Len(); i++ {
if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 {
return c
}
}
return 0
case reflect.Interface:
if c, ok := nilCompare(aVal, bVal); ok {
return c
}
c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type()))
if c != 0 {
return c
}
return compare(aVal.Elem(), bVal.Elem())
default:
// Certain types cannot appear as keys (maps, funcs, slices), but be explicit.
panic("bad type in compare: " + aType.String())
}
}
// nilCompare checks whether either value is nil. If not, the boolean is false.
// If either value is nil, the boolean is true and the integer is the comparison
// value. The comparison is defined to be 0 if both are nil, otherwise the one
// nil value compares low. Both arguments must represent a chan, func,
// interface, map, pointer, or slice.
func nilCompare(aVal, bVal reflect.Value) (int, bool) {
if aVal.IsNil() {
if bVal.IsNil() {
return 0, true
}
return -1, true
}
if bVal.IsNil() {
return 1, true
}
return 0, false
}

View File

@@ -0,0 +1,72 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains the code to handle template options.
package template
import "strings"
// missingKeyAction defines how to respond to indexing a map with a key that is not present.
type missingKeyAction int
const (
mapInvalid missingKeyAction = iota // Return an invalid reflect.Value.
mapZeroValue // Return the zero value for the map element.
mapError // Error out
)
type option struct {
missingKey missingKeyAction
}
// Option sets options for the template. Options are described by
// strings, either a simple string or "key=value". There can be at
// most one equals sign in an option string. If the option string
// is unrecognized or otherwise invalid, Option panics.
//
// Known options:
//
// missingkey: Control the behavior during execution if a map is
// indexed with a key that is not present in the map.
//
// "missingkey=default" or "missingkey=invalid"
// The default behavior: Do nothing and continue execution.
// If printed, the result of the index operation is the string
// "<no value>".
// "missingkey=zero"
// The operation returns the zero value for the map type's element.
// "missingkey=error"
// Execution stops immediately with an error.
func (t *Template) Option(opt ...string) *Template {
t.init()
for _, s := range opt {
t.setOption(s)
}
return t
}
func (t *Template) setOption(opt string) {
if opt == "" {
panic("empty option string")
}
// key=value
if key, value, ok := strings.Cut(opt, "="); ok {
switch key {
case "missingkey":
switch value {
case "invalid", "default":
t.option.missingKey = mapInvalid
return
case "zero":
t.option.missingKey = mapZeroValue
return
case "error":
t.option.missingKey = mapError
return
}
}
}
panic("unrecognized option: " + opt)
}

View File

@@ -0,0 +1,236 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package template
import (
"maps"
"reflect"
"sync"
"text/template/parse"
)
// common holds the information shared by related templates.
type common struct {
tmpl map[string]*Template // Map from name to defined templates.
muTmpl sync.RWMutex // protects tmpl
option option
// We use two maps, one for parsing and one for execution.
// This separation makes the API cleaner since it doesn't
// expose reflection to the client.
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
// Template is the representation of a parsed template. The *parse.Tree
// field is exported only for use by [html/template] and should be treated
// as unexported by all other clients.
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}
// New allocates a new, undefined template with the given name.
func New(name string) *Template {
t := &Template{
name: name,
}
t.init()
return t
}
// Name returns the name of the template.
func (t *Template) Name() string {
return t.name
}
// New allocates a new, undefined template associated with the given one and with the same
// delimiters. The association, which is transitive, allows one template to
// invoke another with a {{template}} action.
//
// Because associated templates share underlying data, template construction
// cannot be done safely in parallel. Once the templates are constructed, they
// can be executed in parallel.
func (t *Template) New(name string) *Template {
t.init()
nt := &Template{
name: name,
common: t.common,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
}
return nt
}
// init guarantees that t has a valid common structure.
func (t *Template) init() {
if t.common == nil {
c := new(common)
c.tmpl = make(map[string]*Template)
c.parseFuncs = make(FuncMap)
c.execFuncs = make(map[string]reflect.Value)
t.common = c
}
}
// Clone returns a duplicate of the template, including all associated
// templates. The actual representation is not copied, but the name space of
// associated templates is, so further calls to [Template.Parse] in the copy will add
// templates to the copy but not to the original. Clone can be used to prepare
// common templates and use them with variant definitions for other templates
// by adding the variants after the clone is made.
func (t *Template) Clone() (*Template, error) {
nt := t.copy(nil)
nt.init()
if t.common == nil {
return nt, nil
}
nt.option = t.option
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
for k, v := range t.tmpl {
if k == t.name {
nt.tmpl[t.name] = nt
continue
}
// The associated templates share nt's common structure.
tmpl := v.copy(nt.common)
nt.tmpl[k] = tmpl
}
t.muFuncs.RLock()
defer t.muFuncs.RUnlock()
maps.Copy(nt.parseFuncs, t.parseFuncs)
maps.Copy(nt.execFuncs, t.execFuncs)
return nt, nil
}
// copy returns a shallow copy of t, with common set to the argument.
func (t *Template) copy(c *common) *Template {
return &Template{
name: t.name,
Tree: t.Tree,
common: c,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
}
}
// AddParseTree associates the argument parse tree with the template t, giving
// it the specified name. If the template has not been defined, this tree becomes
// its definition. If it has been defined and already has that name, the existing
// definition is replaced; otherwise a new template is created, defined, and returned.
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error) {
t.init()
t.muTmpl.Lock()
defer t.muTmpl.Unlock()
nt := t
if name != t.name {
nt = t.New(name)
}
// Even if nt == t, we need to install it in the common.tmpl map.
if t.associate(nt, tree) || nt.Tree == nil {
nt.Tree = tree
}
return nt, nil
}
// Templates returns a slice of defined templates associated with t.
func (t *Template) Templates() []*Template {
if t.common == nil {
return nil
}
// Return a slice so we don't expose the map.
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
m := make([]*Template, 0, len(t.tmpl))
for _, v := range t.tmpl {
m = append(m, v)
}
return m
}
// Delims sets the action delimiters to the specified strings, to be used in
// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template
// definitions will inherit the settings. An empty delimiter stands for the
// corresponding default: {{ or }}.
// The return value is the template, so calls can be chained.
func (t *Template) Delims(left, right string) *Template {
t.init()
t.leftDelim = left
t.rightDelim = right
return t
}
// Funcs adds the elements of the argument map to the template's function map.
// It must be called before the template is parsed.
// It panics if a value in the map is not a function with appropriate return
// type or if the name cannot be used syntactically as a function in a template.
// It is legal to overwrite elements of the map. The return value is the template,
// so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template {
t.init()
t.muFuncs.Lock()
defer t.muFuncs.Unlock()
addValueFuncs(t.execFuncs, funcMap)
addFuncs(t.parseFuncs, funcMap)
return t
}
// Lookup returns the template with the given name that is associated with t.
// It returns nil if there is no such template or the template has no definition.
func (t *Template) Lookup(name string) *Template {
if t.common == nil {
return nil
}
t.muTmpl.RLock()
defer t.muTmpl.RUnlock()
return t.tmpl[name]
}
// Parse parses text as a template body for t.
// Named template definitions ({{define ...}} or {{block ...}} statements) in text
// define additional templates associated with t and are removed from the
// definition of t itself.
//
// Templates can be redefined in successive calls to Parse.
// A template definition with a body containing only white space and comments
// is considered empty and will not replace an existing template's body.
// This allows using Parse to add new named template definitions without
// overwriting the main template body.
func (t *Template) Parse(text string) (*Template, error) {
t.init()
t.muFuncs.RLock()
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins())
t.muFuncs.RUnlock()
if err != nil {
return nil, err
}
// Add the newly parsed trees, including the one for t, into our common structure.
for name, tree := range trees {
if _, err := t.AddParseTree(name, tree); err != nil {
return nil, err
}
}
return t, nil
}
// associate installs the new template into the group of templates associated
// with t. The two are already known to share the common structure.
// The boolean return value reports whether to store this tree as t.Tree.
func (t *Template) associate(new *Template, tree *parse.Tree) bool {
if new.common != t.common {
panic("internal error: associate not common")
}
if old := t.tmpl[new.name]; old != nil && parse.IsEmptyTree(tree.Root) && old.Tree != nil {
// If a template by that name exists,
// don't replace it with an empty template.
return false
}
t.tmpl[new.name] = new
return true
}

View File

@@ -648,6 +648,12 @@ func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
case e.IsNegated():
return p.planNot(e, iter)
case e.IsAnd():
return p.planExprLogicalAnd(e, iter)
case e.IsOr():
return p.planExprLogicalOr(e, iter)
case e.IsCall():
return p.planExprCall(e, iter)
@@ -659,34 +665,34 @@ func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
}
func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
not := &ir.NotStmt{
Block: &ir.Block{},
}
prev := p.curr
p.curr = not.Block
if n, ok := e.Terms.(*ast.Not); ok {
cond := p.newLocal() // success condition
// We're constructing the following plan:
//
// | not
// | | <plan(body)> # assigns Local<cond> = true at each success point
// | | is_defined &{Source:Local<cond>} # aborts inner block if body produced no success
// | iter() # caller's continuation
err := p.planQuery(n.Body, 0, func() error {
p.appendStmt(&ir.AssignVarStmt{
Source: op(ir.Bool(true)),
Target: cond,
})
return nil
})
cond := p.newLocal()
sub, err := p.planBodyAsScope(n.Body, cond)
if err != nil {
return err
}
p.appendStmt(&ir.IsDefinedStmt{
Source: cond,
})
} else {
if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil {
return err
}
sub.Stmts = append(sub.Stmts, &ir.IsDefinedStmt{Source: cond})
p.appendStmt(&ir.NotStmt{Block: sub})
return iter()
}
// Legacy negation
not := &ir.NotStmt{Block: &ir.Block{}}
prev := p.curr
p.curr = not.Block
if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil {
return err
}
p.curr = prev
@@ -695,6 +701,119 @@ func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
return iter()
}
func (p *Planner) planExprLogicalAnd(e *ast.Expr, iter planiter) error {
// We're constructing the following plan:
//
// | reset &{Target:Local<cond>}
// | block lhs
// | | <plan(LHS body)>
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
// | is_defined &{Source:Local<cond>} # aborts outer if LHS produced no success
// | reset &{Target:Local<cond>} # clear before RHS
// | block rhs
// | | <plan(RHS body)>
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
// | is_defined &{Source:Local<cond>} # aborts outer if RHS produced no success
// | iter() # caller's continuation
and := e.Terms.(*ast.LogicalAnd)
cond := p.newLocal() // success condition
if err := planLogicalOperand(p, and.Lhs, cond); err != nil {
return err
}
if err := planLogicalOperand(p, and.Rhs, cond); err != nil {
return err
}
return iter()
}
func planLogicalOperand(p *Planner, body ast.Body, cond ir.Local) error {
p.appendStmt(&ir.ResetLocalStmt{Target: cond})
sub, err := p.planBodyAsScope(body, cond)
if err != nil {
return err
}
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{sub}})
p.appendStmt(&ir.IsDefinedStmt{Source: cond})
return nil
}
func (p *Planner) planExprLogicalOr(e *ast.Expr, iter planiter) error {
// We're constructing the following plan:
//
// | reset &{Target:Local<cond>}
// | block lhs
// | | <plan(LHS body)>
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
// | block outer
// | | block skip
// | | | is_defined &{Source:Local<cond>} # if defined ..
// | | | break &{Index:1} # .. break past RHS
// | | block rhs
// | | | <plan(RHS body)>
// | | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
// | is_defined &{Source:Local<cond>} # aborts outer if neither produced a success
// | iter() # caller's continuation
or := e.Terms.(*ast.LogicalOr)
cond := p.newLocal() // success condition
p.appendStmt(&ir.ResetLocalStmt{Target: cond})
lhsBlock, err := p.planBodyAsScope(or.Lhs, cond)
if err != nil {
return err
}
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{lhsBlock}})
rhsBlock, err := p.planBodyAsScope(or.Rhs, cond)
if err != nil {
return err
}
// skip-rhs-if-lhs-succeeded: if cond is defined, break out past the
// RHS block; otherwise this inner block aborts and the outer block
// falls through into the RHS plan.
skip := &ir.Block{Stmts: []ir.Stmt{
&ir.IsDefinedStmt{Source: cond},
&ir.BreakStmt{Index: 1},
}}
outer := &ir.Block{Stmts: []ir.Stmt{
&ir.BlockStmt{Blocks: []*ir.Block{skip}},
&ir.BlockStmt{Blocks: []*ir.Block{rhsBlock}},
}}
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outer}})
p.appendStmt(&ir.IsDefinedStmt{Source: cond})
return iter()
}
func (p *Planner) planBodyAsScope(body ast.Body, cond ir.Local) (*ir.Block, error) {
sub := &ir.Block{}
prev := p.curr
p.curr = sub
p.vars.Push(map[ast.Var]ir.Local{})
err := p.planQuery(body, 0, func() error {
p.appendStmt(&ir.AssignVarStmt{
Source: op(ir.Bool(true)),
Target: cond,
})
return nil
})
p.vars.Pop()
p.curr = prev
return sub, err
}
func (p *Planner) planWith(e *ast.Expr, iter planiter) error {
// Plan the values that will be applied by the `with` modifiers. All values

View File

@@ -5,6 +5,7 @@ import (
"sort"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
// funcstack implements a simple map structure used to keep track of virtual
@@ -270,18 +271,23 @@ func (t *ruletrie) DepthFirst(f func(*ruletrie) bool) {
}
func (t *ruletrie) Depth() int {
if len(t.Children()) == 0 {
return 0
}
c := make([]int, 0, len(t.Children()))
for _, nodes := range t.children {
c = append(c, nodes[len(nodes)-1].Depth())
}
// Avoid Children()'s slice allocation and sort: we only need the max
// depth over the child nodes. A nil last element is a pushed but
// not-yet-inserted node (see Push), matching Children()'s filter.
max := 0
for i := range c {
if max < c[i] {
max = c[i]
found := false
for _, nodes := range t.children {
last := nodes[len(nodes)-1]
if last == nil {
continue
}
found = true
if d := last.Depth(); d > max {
max = d
}
}
if !found {
return 0
}
return max + 1
}
@@ -291,11 +297,9 @@ func (t *ruletrie) String() string {
}
type functionMocksStack struct {
stack []*functionMocksElem
stack util.GroupStack[frame]
}
type functionMocksElem []frame
type frame map[string]*ast.Term
func newFunctionMocksStack() *functionMocksStack {
@@ -304,30 +308,24 @@ func newFunctionMocksStack() *functionMocksStack {
return stack
}
func newFunctionMocksElem() *functionMocksElem {
return &functionMocksElem{}
}
func (s *functionMocksStack) Push() {
s.stack = append(s.stack, newFunctionMocksElem())
s.stack.PushGroup(nil)
}
func (s *functionMocksStack) Pop() {
s.stack = s.stack[:len(s.stack)-1]
s.stack.PopGroup()
}
func (s *functionMocksStack) PushFrame(f frame) {
current := s.stack[len(s.stack)-1]
*current = append(*current, f)
s.stack.Push(f)
}
func (s *functionMocksStack) PopFrame() {
current := s.stack[len(s.stack)-1]
*current = (*current)[:len(*current)-1]
s.stack.Pop()
}
func (s *functionMocksStack) Lookup(f string) *ast.Term {
current := *s.stack[len(s.stack)-1]
current := s.stack.PeekGroup()
for i := len(current) - 1; i >= 0; i-- {
if t, ok := current[i][f]; ok {
return t

View File

@@ -101,10 +101,10 @@ func Compare(a, b string) int {
return aV.Compare(bV)
}
// AppendText appends the textual representation of the version to b and returns the extended buffer.
// AppendString appends the textual representation of the version to b and returns the extended buffer.
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
// without allocating, provided the caller has pre-allocated sufficient space in b.
func (v Version) AppendText(b []byte) ([]byte, error) {
func (v Version) AppendString(b []byte) ([]byte, error) {
if b == nil {
b = make([]byte, 0, length(v))
}
@@ -126,7 +126,7 @@ func (v Version) AppendText(b []byte) ([]byte, error) {
// String returns the string representation of the version.
func (v Version) String() string {
bs := make([]byte, 0, length(v))
bs, _ = v.AppendText(bs)
bs, _ = v.AppendString(bs)
return string(bs)
}

View File

@@ -13,7 +13,6 @@ import (
"strings"
"github.com/open-policy-agent/opa/internal/deepcopy"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
@@ -40,8 +39,8 @@ type (
Labels map[string]any `json:"labels,omitempty"`
Location *Location `json:"location,omitempty"`
comments []*Comment
node Node
endLoc *Location
node Node
}
// SchemaAnnotation contains a schema declaration for the document identified by the path.
@@ -108,11 +107,10 @@ func (a *Annotations) SetLoc(l *Location) {
// EndLoc returns the location of this annotation's last comment line.
func (a *Annotations) EndLoc() *Location {
count := len(a.comments)
if count == 0 {
if a.endLoc == nil {
return a.Location
}
return a.comments[count-1].Location
return a.endLoc
}
// Compare returns an integer indicating if a is less than, equal to, or greater
@@ -193,64 +191,6 @@ func (a *Annotations) GetTargetPath() Ref {
}
}
func (a *Annotations) MarshalJSON() ([]byte, error) {
if a == nil {
return []byte(`{"scope":""}`), nil
}
data := map[string]any{
"scope": a.Scope,
}
if a.Title != "" {
data["title"] = a.Title
}
if a.Description != "" {
data["description"] = a.Description
}
if a.Entrypoint {
data["entrypoint"] = a.Entrypoint
}
if len(a.Organizations) > 0 {
data["organizations"] = a.Organizations
}
if len(a.RelatedResources) > 0 {
data["related_resources"] = a.RelatedResources
}
if len(a.Authors) > 0 {
data["authors"] = a.Authors
}
if len(a.Schemas) > 0 {
data["schemas"] = a.Schemas
}
if a.Compile != nil {
data["compile"] = a.Compile
}
if len(a.Custom) > 0 {
data["custom"] = a.Custom
}
if len(a.Labels) > 0 {
data["labels"] = a.Labels
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
if a.Location != nil {
data["location"] = a.Location
}
}
return json.Marshal(data)
}
func NewAnnotationsRef(a *Annotations) *AnnotationsRef {
var loc *Location
if a.node != nil {
@@ -285,34 +225,6 @@ func (ar *AnnotationsRef) GetRule() *Rule {
}
}
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": ar.Path,
}
if ar.Annotations != nil {
data["annotations"] = ar.Annotations
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
if ar.Location != nil {
data["location"] = ar.Location
}
// The location set for the schema ref terms is wrong (always set to
// row 1) and not really useful anyway.. so strip it out before marshalling
for _, schema := range ar.Annotations.Schemas {
if schema.Path != nil {
for _, term := range schema.Path {
term.Location = nil
}
}
}
}
return json.Marshal(data)
}
func scopeCompare(s1, s2 string) int {
o1 := scopeOrder(s1)
o2 := scopeOrder(s2)
@@ -698,18 +610,6 @@ func (rr *RelatedResourceAnnotation) String() string {
return string(bs)
}
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
d := map[string]any{
"ref": rr.Ref.String(),
}
if len(rr.Description) > 0 {
d["description"] = rr.Description
}
return json.Marshal(d)
}
// Copy returns a deep copy of s.
func (s *SchemaAnnotation) Copy() *SchemaAnnotation {
cpy := *s

View File

@@ -0,0 +1,124 @@
//go:build !go1.27
package ast
import (
"encoding/json"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
)
func (a *Annotations) MarshalJSON() ([]byte, error) {
if a == nil {
return []byte(`{"scope":""}`), nil
}
data := map[string]any{
"scope": a.Scope,
}
if a.Title != "" {
data["title"] = a.Title
}
if a.Description != "" {
data["description"] = a.Description
}
if a.Entrypoint {
data["entrypoint"] = a.Entrypoint
}
if len(a.Organizations) > 0 {
data["organizations"] = a.Organizations
}
if len(a.RelatedResources) > 0 {
data["related_resources"] = a.RelatedResources
}
if len(a.Authors) > 0 {
data["authors"] = a.Authors
}
if len(a.Schemas) > 0 {
data["schemas"] = a.Schemas
}
if a.Compile != nil {
data["compile"] = a.Compile
}
if len(a.Custom) > 0 {
data["custom"] = a.Custom
}
if len(a.Labels) > 0 {
data["labels"] = a.Labels
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
if a.Location != nil {
data["location"] = a.Location
}
}
return json.Marshal(data)
}
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
d := map[string]any{
"ref": rr.Ref.String(),
}
if len(rr.Description) > 0 {
d["description"] = rr.Description
}
return json.Marshal(d)
}
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": ar.Path,
}
if ar.Annotations != nil {
data["annotations"] = ar.Annotations
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
if ar.Location != nil {
data["location"] = ar.Location
}
}
return json.Marshal(data)
}
// schemaAnnotationJSON mirrors SchemaAnnotation's JSON tags, with location-free
// path terms.
type schemaAnnotationJSON struct {
Path []termJSON `json:"path"`
Schema Ref `json:"schema,omitempty"`
Definition *any `json:"definition,omitempty"`
}
func (s *SchemaAnnotation) MarshalJSON() ([]byte, error) {
d := schemaAnnotationJSON{
Schema: s.Schema,
Definition: s.Definition,
}
if s.Path != nil {
d.Path = make([]termJSON, len(s.Path))
for i, t := range s.Path {
// The location is omitted: path terms are parsed on their own from
// the annotation's YAML key, so their locations are offsets into that
// key (always row 1) rather than positions in the module.
d.Path[i] = termJSON{Type: ValueName(t.Value), Value: t.Value}
}
}
return json.Marshal(d)
}

View File

@@ -0,0 +1,195 @@
//go:build go1.27
package ast
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"github.com/open-policy-agent/opa/internal/jsonv2"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
)
// These are exported types, so losing MarshalJSON here would be a breaking
// API change even though callers should go through json.Marshal, not this
// method directly.
var (
_ json.Marshaler = &Annotations{}
_ json.Marshaler = &AnnotationsRef{}
_ json.Marshaler = &SchemaAnnotation{}
_ json.Marshaler = &RelatedResourceAnnotation{}
)
func (a *Annotations) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if a == nil {
e.WriteToken(jsontext.String("scope"))
e.WriteToken(jsontext.String(""))
return e.WriteToken(jsontext.EndObject)
}
if a.Description != "" {
e.WriteToken(jsontext.String("description"))
e.WriteToken(jsontext.String(a.Description))
}
if a.Entrypoint {
e.WriteToken(jsontext.String("entrypoint"))
e.WriteToken(jsontext.True)
}
if len(a.Organizations) > 0 {
if err := jsonv2.WriteFieldValue(e, "organizations", a.Organizations); err != nil {
return err
}
}
if len(a.RelatedResources) > 0 {
if err := jsonv2.WriteFieldArray(e, "related_resources", a.RelatedResources); err != nil {
return err
}
}
if len(a.Authors) > 0 {
if err := jsonv2.WriteFieldValue(e, "authors", a.Authors); err != nil {
return err
}
}
if len(a.Schemas) > 0 {
if err := jsonv2.WriteFieldArray(e, "schemas", a.Schemas); err != nil {
return err
}
}
if a.Compile != nil {
if err := jsonv2.WriteFieldValue(e, "compile", a.Compile); err != nil {
return err
}
}
if len(a.Custom) > 0 {
if err := jsonv2.WriteFieldValue(e, "custom", a.Custom); err != nil {
return err
}
}
if len(a.Labels) > 0 {
if err := jsonv2.WriteFieldValue(e, "labels", a.Labels); err != nil {
return err
}
}
e.WriteToken(jsontext.String("scope"))
e.WriteToken(jsontext.String(a.Scope))
if a.Title != "" {
e.WriteToken(jsontext.String("title"))
e.WriteToken(jsontext.String(a.Title))
}
if a.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
if err := jsonv2.WriteField(e, "location", a.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (a *Annotations) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(a)
}
func (ar *AnnotationsRef) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if ar.Annotations != nil {
if err := jsonv2.WriteField(e, "annotations", ar.Annotations); err != nil {
return err
}
}
if ar.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
if err := jsonv2.WriteField(e, "location", ar.Location); err != nil {
return err
}
}
if err := jsonv2.WriteField(e, "path", ar.Path); err != nil {
return err
}
return e.WriteToken(jsontext.EndObject)
}
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(ar)
}
func (s *SchemaAnnotation) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(s)
}
func (s *SchemaAnnotation) MarshalJSONTo(e *jsontext.Encoder) error {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginObject)
// Path has no omitempty tag, so it's always written. A nil ref is written
// as null, matching encoding/json v1's treatment of a nil slice.
e.WriteToken(jsontext.String("path"))
if s.Path == nil {
e.WriteToken(jsontext.Null)
} else {
e.WriteToken(jsontext.BeginArray)
for _, t := range s.Path {
// The location is omitted: path terms are parsed on their own from
// the annotation's YAML key, so their locations are offsets into that
// key (always row 1) rather than positions in the module.
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("type"))
e.WriteToken(jsontext.String(ValueName(t.Value)))
e.WriteToken(jsontext.String("value"))
if err := marshalValueTo(e, t.Value); err != nil {
return fmt.Errorf("failed to marshal schema path term of %s: %w", ValueName(t.Value), err)
}
e.WriteToken(jsontext.EndObject)
}
e.WriteToken(jsontext.EndArray)
}
if len(s.Schema) > 0 {
if err := jsonv2.WriteField(e, "schema", s.Schema); err != nil {
return err
}
}
if s.Definition != nil {
if err := jsonv2.WriteFieldValue(e, "definition", s.Definition); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(rr)
}
func (rr *RelatedResourceAnnotation) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("ref"))
e.WriteToken(jsontext.String(rr.Ref.String()))
if len(rr.Description) > 0 {
e.WriteToken(jsontext.String("description"))
e.WriteToken(jsontext.String(rr.Description))
}
return e.WriteToken(jsontext.EndObject)
}

View File

@@ -141,6 +141,7 @@ var DefaultBuiltins = [...]*Builtin{
StartsWith,
EndsWith,
Split,
SplitN,
Replace,
ReplaceN,
Trim,
@@ -1281,6 +1282,21 @@ var Split = &Builtin{
CanSkipBctx: true,
}
var SplitN = &Builtin{
Name: "strings.split_n",
Description: "Returns an array of at most `n` parts of `x` split on `delimiter`. If `n` is positive, returns the first `n` parts. If `n` is negative, returns the last `abs(n)` parts. If `n` is zero, returns an empty array. If `abs(n)` exceeds the number of parts, all parts are returned.",
Decl: types.NewFunction(
types.Args(
types.Named("x", types.S).Description("string that is split"),
types.Named("delimiter", types.S).Description("delimiter used for splitting"),
types.Named("n", types.N).Description("number of parts to return; positive selects from the left, negative from the right, zero returns an empty array"),
),
types.Named("ys", types.NewArray(nil, types.S)).Description("split parts"),
),
Categories: stringsCat,
CanSkipBctx: true,
}
var Replace = &Builtin{
Name: "replace",
Description: "Replace replaces all instances of a sub-string.",
@@ -1624,7 +1640,10 @@ var JSONFilter = &Builtin{
),
)).Description("JSON string paths"),
),
types.Named("filtered", types.A).Description("remaining data from `object` with only keys specified in `paths`"),
types.Named("filtered", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("remaining data from `object` with only keys specified in `paths`"),
),
Categories: objectCat,
CanSkipBctx: true,
@@ -1663,7 +1682,10 @@ var JSONRemove = &Builtin{
),
)).Description("JSON string paths"),
),
types.Named("output", types.A).Description("result of removing all keys specified in `paths`"),
types.Named("output", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("result of removing all keys specified in `paths`"),
),
Categories: objectCat,
CanSkipBctx: true,
@@ -1724,7 +1746,7 @@ var ObjectSubset = &Builtin{
types.NewArray(nil, types.A),
)).Description("object to test if super is a superset of"),
),
types.Named("result", types.A).Description("`true` if `sub` is a subset of `super`"),
types.Named("result", types.B).Description("`true` if `sub` is a subset of `super`, otherwise undefined"),
),
CanSkipBctx: true,
}
@@ -1744,8 +1766,11 @@ var ObjectUnion = &Builtin{
types.NewDynamicProperty(types.A, types.A),
)).Description("right-hand object"),
),
types.Named("output", types.A).Description("a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object `b`"),
), // TODO(sr): types.A? ^^^^^^^ (also below)
types.Named("output", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object `b`"),
),
CanSkipBctx: true,
}
@@ -1760,7 +1785,10 @@ var ObjectUnionN = &Builtin{
types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
)).Description("list of objects to merge"),
),
types.Named("output", types.A).Description("asymmetric recursive union of all objects in `objects`, merged from left to right, where conflicts are resolved by choosing the key from the right-hand object"),
types.Named("output", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("asymmetric recursive union of all objects in `objects`, merged from left to right, where conflicts are resolved by choosing the key from the right-hand object"),
),
CanSkipBctx: true,
}
@@ -1780,7 +1808,10 @@ var ObjectRemove = &Builtin{
types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
)).Description("keys to remove from x"),
),
types.Named("output", types.A).Description("result of removing the specified `keys` from `object`"),
types.Named("output", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("result of removing the specified `keys` from `object`"),
),
CanSkipBctx: true,
}
@@ -1801,7 +1832,10 @@ var ObjectFilter = &Builtin{
types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
)).Description("keys to keep in `object`"),
),
types.Named("filtered", types.A).Description("remaining data from `object` with only keys specified in `keys`"),
types.Named("filtered", types.NewObject(
nil,
types.NewDynamicProperty(types.A, types.A),
)).Description("remaining data from `object` with only keys specified in `keys`"),
),
CanSkipBctx: true,
}

View File

@@ -1238,8 +1238,8 @@ func removeDuplicate(list []Value) []Value {
return newResult
}
func getArgTypes(env *TypeEnv, args []*Term) []types.Type {
pre := make([]types.Type, len(args))
func getArgTypes(env *TypeEnv, args []*Term) (pre []types.Type) {
pre = make([]types.Type, len(args))
for i := range args {
pre[i] = env.GetByValue(args[i].Value)
}

View File

@@ -336,26 +336,18 @@ func TermValueCompare(a, b *Term) int {
return a.Value.Compare(b.Value)
}
func TermValueEqual(a, b *Term) bool {
return ValueEqual(a.Value, b.Value)
}
func ValueEqual(a, b Value) bool {
switch v := a.(type) {
case Null:
return v.Equal(b)
case Boolean:
return v.Equal(b)
case Null, Boolean, String, Var:
return v == b
case Number:
return v.Equal(b)
case String:
return v.Equal(b)
case Var:
return v.Equal(b)
case Ref:
return v.Equal(b)
case *Array:
return v.Equal(b)
case *Not:
return v.Equal(b)
case *TemplateString:
return v.Equal(b)
}

View File

@@ -1344,9 +1344,12 @@ func (c *Compiler) checkRuleConflicts() {
kinds := make(map[RuleKind]struct{}, len(rules))
completeRules := 0
partialRules := 0
// `p contains x` (set) vs `p[k] contains v` (object of sets): a mix is a conflict.
var hasMultiValueSet bool
var hasMultiValueObject bool
arities := make(map[int]struct{}, len(rules))
name := ""
var conflicts []Ref
var conflicts []ruleRef
defaultRules := make([]*Rule, 0)
for _, rule := range rules {
@@ -1403,6 +1406,15 @@ func (c *Compiler) checkRuleConflicts() {
} else {
partialRules++
}
if r.Head.RuleKind() == MultiValue {
// A ground ref ends at the node (set); a longer one extends past it (object).
if ref.IsGround() {
hasMultiValueSet = true
} else {
hasMultiValueObject = true
}
}
}
// Functions cannot exist within a rule's dynamic extent, as there is no valid
@@ -1422,9 +1434,9 @@ func (c *Compiler) checkRuleConflicts() {
switch {
case conflicts != nil:
return !c.err(NewError(TypeErr, rules[0].Loc(), "rule %v conflicts with %v", name, conflicts))
return !c.err(NewError(TypeErr, rules[0].Loc(), "rule %v conflicts with%v", name, formatConflict(conflicts, rw)))
case len(kinds) > 1 || len(arities) > 1 || (completeRules >= 1 && partialRules >= 1):
case len(kinds) > 1 || len(arities) > 1 || (completeRules >= 1 && partialRules >= 1) || (hasMultiValueSet && hasMultiValueObject):
return !c.err(NewError(TypeErr, rules[0].Loc(), "conflicting rules %v found", name))
case len(defaultRules) > 1:
@@ -1596,11 +1608,15 @@ func (c *Compiler) checkSafetyRuleHeads() {
if vars.DiffCount(vis.vars) > 0 {
unsafe := vars.Diff(vis.vars)
for v := range unsafe {
// vars is keyed by the original name, so the location must be
// read before v is replaced with the rewritten one -- otherwise
// the lookup misses and the error is reported without a location.
loc := vars[v].Location
if w, ok := c.RewrittenVars[v]; ok {
v = w
}
if !v.IsGenerated() {
if !c.err(NewError(UnsafeVarErr, vars[v].Location, "var %v is unsafe", v)) {
if !c.err(NewError(UnsafeVarErr, loc, "var %v is unsafe", v)) {
return true
}
}
@@ -1701,16 +1717,17 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s
// Handle referenced schemas, returns directly when a $ref is found
if subSchema.RefSchema != nil {
if existing, ok := parser.definitionCache[subSchema.Ref.String()]; ok {
subSchemaStr := subSchema.Ref.String()
if existing, ok := parser.definitionCache[subSchemaStr]; ok {
if existing.processing {
if existing.rec == nil {
existing.rec = types.NewRecursive(subSchema.Ref.String(), nil)
existing.rec = types.NewRecursive(subSchemaStr, nil)
}
return existing.rec, nil
}
return existing.typ, nil
}
return parser.parseSchemaWithPropertyKey(subSchema.RefSchema, subSchema.Ref.String())
return parser.parseSchemaWithPropertyKey(subSchema.RefSchema, subSchemaStr)
}
// Cache this $ref definition and finalize it via defer when parsing
@@ -2218,7 +2235,7 @@ func (c *Compiler) resolveAllRefs() {
}
for v, u := range globals {
if v.Equal(imp.Name()) && !u.used {
if v == imp.Name() && !u.used {
if !c.err(NewError(CompileErr, imp.Location, "%s unused", imp.String())) {
return
}
@@ -3327,6 +3344,50 @@ func (c *Compiler) rewriteLocalVarsInRule(rule *Rule, unusedArgs VarSet, argsSta
stack := argsStack.Copy()
// A variable shadowing a built-in name (e.g. `count`) is allowed in Rego,
// but if left un-rewritten later stages (type checking, arity, partial
// eval) can mistake it for the built-in, causing spurious,
// map-order-dependent errors (issue #3729). Rewrite such variables to
// fresh locals, like `:=`-declared ones.
//
// Only variables bound in the body are rewritten. Excluded: head-only
// references (stay unsafe-var errors), call operators (SkipRefCallHead),
// and `with` targets/values (possible function mocks).
if len(c.builtins) > 0 {
bodyVis := NewVarVisitor().WithParams(VarVisitorParams{
SkipRefCallHead: true,
SkipClosures: true,
})
bodyVis.Walk(rule.Body)
bodyVars := bodyVis.Vars()
declaredInBody := declaredVars(rule.Body)
withVars := NewVarSet()
NewGenericVisitor(func(x any) bool {
if w, ok := x.(*With); ok {
WalkVars(w, func(v Var) bool {
withVars.Add(v)
return false
})
}
return false
}).Walk(rule)
for _, v := range bodyVars.Sorted() {
if _, ok := c.builtins[v.String()]; !ok {
continue
}
if declaredInBody.Contains(v) || withVars.Contains(v) {
continue
}
if _, ok := stack.Declared(v); ok {
continue
}
stack.Insert(v, gen.Generate(), seenVar)
}
}
body, declared, errs := rewriteLocalVars(gen, stack, used, rule.Body, c.strict)
// For rewritten vars use the collection of all variables that
@@ -4285,26 +4346,61 @@ func (n *TreeNode) add(path Ref, val any) {
}
}
// ExternalIndex ties an ExternalRuleSource-provided index to the package Ref it
// serves. It is internal plumbing exported only so the topdown evaluator can
// reach it across the ast/topdown package boundary; it is not part of OPA's
// supported public API and may change without notice. The stable surface for
// implementing external rule sources is the ExternalRuleSource and
// ExternalRuleIndex interfaces.
type ExternalIndex struct {
Index ExternalRuleIndex
Ref Ref
}
func (ei *ExternalIndex) Tree(ctx context.Context, rt *TreeNode, prefix Ref, input *Term, m metrics.Metrics, reqMD map[string]any, respMD map[string]any) (*TreeNode, ExternalRuleIndex, error) {
resolver := &termResolver{input: input}
// Tree resolves external rules for prefix, using resolver to resolve references
// while building search queries. Passing a save-set-aware resolver (e.g. the
// topdown evaluator) lets sources that opt into
// ExternalSourceOptions.DistinguishAbsentFromUnknown distinguish absent input
// from values that are unknown under partial evaluation.
//
// params carries the ground key values that followed the registered prefix for
// a parametrized source (see ParametrizedExternalRuleIndex); it is nil for
// conventional sources. The returned subtree is always rooted at prefix (the
// registered ref), regardless of params — the evaluator layers the parameter
// levels back on top.
//
// Like ExternalIndex, Tree is internal plumbing exported only for the topdown
// evaluator. It is not part of OPA's supported public API and may change
// without notice.
func (ei *ExternalIndex) Tree(ctx context.Context, rt *TreeNode, prefix Ref, params []Value, resolver ValueResolver, m metrics.Metrics, reqMD map[string]any, respMD map[string]any) (*TreeNode, ExternalRuleIndex, error) {
o := ei.Index.Opts()
// Select the resolver handed to the source. By default we wrap the caller's
// resolver so external sources see the legacy behavior (absent and unknown
// both collapse to UnknownValueErr, non-input refs are never resolved).
// Sources that set DistinguishAbsentFromUnknown receive the caller's
// save-set-aware resolver unchanged, letting them tell absent from unknown.
lookupResolver := resolver
switch {
case lookupResolver == nil:
lookupResolver = unknownResolver{}
case o == nil || !o.DistinguishAbsentFromUnknown:
lookupResolver = legacyExternalResolver{inner: lookupResolver}
}
rules, updatedIndex, err := ei.Index.Lookup(ctx,
LookupResolver(resolver),
LookupResolver(lookupResolver),
LookupMetrics(m),
LookupRequestMetadata(reqMD),
LookupResponseMetadata(respMD),
LookupParams(params),
)
if err != nil {
return nil, nil, err
}
c0 := NewCompiler()
if o := ei.Index.Opts(); o != nil {
if o != nil {
if len(o.SkippedStages) > 0 {
c0.WithSkipStages(o.SkippedStages...)
}
@@ -4343,24 +4439,37 @@ func (ei *ExternalIndex) Tree(ctx context.Context, rt *TreeNode, prefix Ref, inp
return node, updatedIndex, nil
}
type termResolver struct {
input *Term
// legacyExternalResolver reproduces the historical external-source resolver
// behavior on top of an arbitrary (typically save-set-aware) resolver: only
// input references are resolvable, and any input reference that does not
// resolve to a concrete value is reported as UnknownValueErr. This collapses
// "absent from the concrete input" and "symbolic under partial evaluation"
// into a single signal, matching what external sources saw before
// ExternalSourceOptions.DistinguishAbsentFromUnknown existed.
type legacyExternalResolver struct {
inner ValueResolver
}
func (r *termResolver) Resolve(ref Ref) (Value, error) {
if ref.HasPrefix(InputRootRef) {
if r.input == nil {
return nil, UnknownValueErr{}
}
v, err := r.input.Value.Find(ref[1:])
if err != nil {
return nil, UnknownValueErr{}
}
return v, nil
func (r legacyExternalResolver) Resolve(ref Ref) (Value, error) {
if !ref.HasPrefix(InputRootRef) {
return nil, UnknownValueErr{}
}
return nil, UnknownValueErr{}
v, err := r.inner.Resolve(ref)
if err != nil {
return nil, err
}
if v == nil {
return nil, UnknownValueErr{}
}
return v, nil
}
// unknownResolver treats every reference as unknown. It is used as a safe
// fallback when Tree is invoked without a resolver.
type unknownResolver struct{}
func (unknownResolver) Resolve(Ref) (Value, error) { return nil, UnknownValueErr{} }
// Size returns the number of rules in the tree.
func (n *TreeNode) Size() (s int) {
for _, c := range n.Children {
@@ -4481,30 +4590,50 @@ func attachValueToNode(node *TreeNode, ref Ref, val any) {
}
}
type ruleRef struct {
ref Ref
loc *Location
}
// flattenChildren flattens all children's rule refs into a sorted array.
func (n *TreeNode) flattenChildren() []Ref {
func (n *TreeNode) flattenChildren() []ruleRef {
return n.flattenMatchingChildren(func(_ *Rule) bool { return true })
}
// flattenChildFunctions is like flattenChildren but only collects functions (rules with args).
func (n *TreeNode) flattenChildFunctions() []Ref {
func (n *TreeNode) flattenChildFunctions() []ruleRef {
return n.flattenMatchingChildren(func(r *Rule) bool { return r.isFunction() })
}
func (n *TreeNode) flattenMatchingChildren(f func(*Rule) bool) []Ref {
ret := newRefSet()
func (n *TreeNode) flattenMatchingChildren(f func(*Rule) bool) []ruleRef {
var ret ruleRefSet
for _, sub := range n.Children { // we only want the children, so don't use n.DepthFirst() right away
sub.DepthFirst(func(x *TreeNode) bool {
for _, rule := range x.Values {
if f(rule) {
ret.AddPrefix(rule.Ref())
ret.AddPrefix(ruleRef{ref: rule.Ref(), loc: rule.Loc()})
}
}
return false
})
}
return util.SortedFunc(ret.s, RefCompare)
return util.SortedFunc(ret.s, func(a, b ruleRef) int {
return RefCompare(a.ref, b.ref)
})
}
func formatConflict(conflicts []ruleRef, rw varRewriter) string {
s := strings.Builder{}
s.WriteString(":\n")
for _, conflict := range conflicts {
s.WriteString(" rule ")
s.WriteString(rw(conflict.ref.Copy()).String())
s.WriteString(" at ")
s.WriteString(conflict.loc.String())
s.WriteString("\n")
}
return strings.TrimSuffix(s.String(), "\n")
}
// Copy creates a shallow copy of the TreeNode suitable for augmentation.
@@ -4639,7 +4768,12 @@ func (g *Graph) Sort() (sorted []util.T, ok bool) {
temp: map[util.T]struct{}{},
}
nodesList := make([]util.T, 0, len(g.nodes))
for node := range g.nodes {
nodesList = append(nodesList, node)
}
sortGraphNodes(nodesList)
for _, node := range nodesList {
if !sorter.Visit(node) {
return nil, false
}
@@ -4687,6 +4821,24 @@ type graphSort struct {
temp map[util.T]struct{}
}
// sortGraphNodes orders rule nodes deterministically (by location, then ref)
// so the topological sort, and thus the rule type-checking order, doesn't
// depend on Go's randomized map iteration (issue #3729). Head.Ref is used for
// the tie-break rather than Rule.Ref so nodes with a nil Module don't panic.
func sortGraphNodes(nodes []util.T) {
slices.SortStableFunc(nodes, func(a, b util.T) int {
ra, aok := a.(*Rule)
rb, bok := b.(*Rule)
if !aok || !bok {
return 0
}
if c := ra.Location.Compare(rb.Location); c != 0 {
return c
}
return ra.Head.Ref().Compare(rb.Head.Ref())
})
}
func (sort *graphSort) Marked(node util.T) bool {
_, marked := sort.marked[node]
return marked
@@ -4700,7 +4852,13 @@ func (sort *graphSort) Visit(node util.T) (ok bool) {
return true
}
sort.temp[node] = struct{}{}
for other := range sort.deps(node) {
deps := sort.deps(node)
depList := make([]util.T, 0, len(deps))
for other := range deps {
depList = append(depList, other)
}
sortGraphNodes(depList)
for _, other := range depList {
if !sort.Visit(other) {
return false
}
@@ -5249,6 +5407,15 @@ func outputVarsForExprEq(expr *Expr, safe VarSet, output VarSet) VarSet {
output = outputVarsForTerms(expr, safe, output)
output.Update(safe)
if expr.fromAssignment {
// The LHS of `:=` is a pure output; excluding it from the safe basis
// stops the RHS being made safe by unifying backwards through the LHS.
// See issue #3546.
WalkVars(expr.Operand(0), func(v Var) bool {
delete(output, v)
return false
})
}
output.Update(Unify(output, expr.Operand(0), expr.Operand(1)))
diff := output.Diff(safe)
@@ -6368,7 +6535,7 @@ func (s localDeclaredVars) Insert(x, y Var, occurrence varOccurrence) {
// If the variable has been rewritten (where x != y, with y being
// the generated value), store it in the map of rewritten vars.
// Assume that the generated values are unique for the compilation.
if !x.Equal(y) {
if x != y {
s.rewritten[y] = x
}
}
@@ -6817,6 +6984,7 @@ func rewriteDeclaredAssignment(g *localVarGenerator, stack *localDeclaredVars, e
if len(errs) == numErrsBefore {
loc := expr.Operator()[0].Location
expr.SetOperator(RefTerm(VarTerm(Equality.Name).SetLocation(loc)).SetLocation(loc))
expr.fromAssignment = true
}
return expr, errs
@@ -6845,7 +7013,7 @@ func rewriteDeclaredVarsInTerm(g *localVarGenerator, stack *localDeclaredVars, t
case Call:
ref := v[0]
WalkVars(ref, func(v Var) bool {
if gv, ok := stack.Declared(v); ok && !gv.Equal(v) {
if gv, ok := stack.Declared(v); ok && gv != v {
// We will rewrite the ref of a function call, which is never ok since we don't have first-class functions.
errs = append(errs, NewError(CompileErr, term.Location, "called function %s shadowed", ref))
return true
@@ -6900,11 +7068,11 @@ func rewriteDeclaredVarsInWithRecursive(g *localVarGenerator, stack *localDeclar
if sdwInput, ok := stack.Declared(InputRootDocument.Value.(Var)); ok { // Was "input" shadowed...
switch value := w.Target.Value.(type) {
case Var:
if sdwInput.Equal(value) { // ...and replaced? If so, fix it
if sdwInput == value { // ...and replaced? If so, fix it
w.Target.Value = InputRootRef
}
case Ref:
if sdwInput.Equal(value[0].Value.(Var)) {
if sdwInput.Equal(value[0].Value) {
w.Target.Value.(Ref)[0].Value = InputRootDocument.Value
}
}
@@ -7233,48 +7401,24 @@ func rewriteVarsInRef(vars ...map[Var]Var) varRewriter {
}
}
// NOTE(sr): This is duplicated with compile/compile.go; but moving it into another location
// would cause a circular dependency -- the refSet definition needs ast.Ref. If we make it
// public in the ast package, the compile package could take it from there, but it would also
// increase our public interface. Let's reconsider if we need it in a third place.
type refSet struct {
s []Ref
}
func newRefSet(x ...Ref) *refSet {
result := &refSet{}
for i := range x {
result.AddPrefix(x[i])
}
return result
}
// ContainsPrefix returns true if r is prefixed by any of the existing refs in the set.
func (rs *refSet) ContainsPrefix(r Ref) bool {
return slices.ContainsFunc(rs.s, r.HasPrefix)
type ruleRefSet struct {
s []ruleRef
}
// AddPrefix inserts r into the set if r is not prefixed by any existing
// refs in the set. If any existing refs are prefixed by r, those existing
// refs are removed.
func (rs *refSet) AddPrefix(r Ref) {
if rs.ContainsPrefix(r) {
return
}
cpy := []Ref{r}
func (rs *ruleRefSet) AddPrefix(r ruleRef) {
for i := range rs.s {
if !rs.s[i].HasPrefix(r) {
if r.ref.HasPrefix(rs.s[i].ref) {
return
}
}
cpy := []ruleRef{r}
for i := range rs.s {
if !rs.s[i].ref.HasPrefix(r.ref) {
cpy = append(cpy, rs.s[i])
}
}
rs.s = cpy
}
// Sorted returns a sorted slice of terms for refs in the set.
func (rs *refSet) Sorted() []*Term {
terms := make([]*Term, len(rs.s))
for i := range rs.s {
terms[i] = NewTerm(rs.s[i])
}
return util.SortedFunc(terms, TermValueCompare)
}

View File

@@ -87,7 +87,6 @@ type Error struct {
}
func (e *Error) Error() string {
var prefix string
if e.Location != nil {
@@ -119,6 +118,34 @@ func (e *Error) Error() string {
return sb.String()
}
func (e *Error) Equal(other *Error) bool {
if e == other {
return true
}
if e == nil || other == nil {
return false
}
if e.Code != other.Code || e.Message != other.Message {
return false
}
if !e.Location.Equal(other.Location) {
return false
}
if (e.Details == nil) != (other.Details == nil) {
return false
}
if e.Details != nil && !slices.Equal(e.Details.Lines(), other.Details.Lines()) {
return false
}
return true
}
// NewError returns a new Error object.
func NewError(code string, loc *Location, f string, a ...any) *Error {
return newErrorString(code, loc, fmt.Sprintf(f, a...))

View File

@@ -46,6 +46,39 @@ type ExternalRuleIndexCloser interface {
Close() error
}
// ParametrizedExternalRuleIndex is an optional interface implemented by external
// rule indexes that serve a family of sub-references under their registered
// prefix rather than a single exact ref. The Ref such a source is registered
// under is treated as a PREFIX: the leading elements of a query reference that
// follow the prefix are consumed as ground lookup parameters (handed to Lookup
// via LookupOptions.Params) rather than as descents into a static rule tree.
// This lets one registered source serve an unbounded family of sub-references —
// one distinct set of rules per parameter tuple — without registering each
// concretely, so references whose key only comes into existence at runtime
// resolve without a recompile.
//
// An index that does not implement this interface behaves as a conventional
// exact-ref source (equivalent to an arity of 0).
type ParametrizedExternalRuleIndex interface {
ExternalRuleIndex
// ParamArity reports how many elements following the registered prefix this
// index consumes as lookup parameters, given the reference tail (the query
// reference elements after the prefix, or an empty Ref when none follow).
//
// The count may vary with the tail's *shape* — e.g. keying off a leading
// discriminator segment — which lets a single prefix back an uneven-depth
// tree. It must NOT depend on parameter *values*: ParamArity is consulted
// before the parameters are plugged, so the tail may contain non-ground
// elements, and the count decides the caching boundary. Returning 0 makes
// the reference resolve as a conventional exact ref.
//
// The parameter elements the count selects must be ground at evaluation
// time. A non-ground parameter yields an undefined result, except under
// partial evaluation where the reference is saved for residualization.
ParamArity(tail Ref) int
}
// ExternalSourceOptions contains options for registering an external rule source.
type ExternalSourceOptions struct {
// VisibleRefs controls which parts of the surrounding rule tree the external
@@ -68,6 +101,25 @@ type ExternalSourceOptions struct {
// This is forward-compatible: new compiler stages added in future releases
// will be skipped automatically rather than running unexpectedly.
SkippedStages []StageID
// DistinguishAbsentFromUnknown controls how the resolver passed to Lookup
// (via LookupOptions.Resolver) reports references that do not resolve to a
// concrete value.
//
// When false (default), the legacy behavior is preserved for backwards
// compatibility: only input references are resolvable, and any input
// reference that cannot be resolved — whether it is genuinely absent from a
// concrete input or symbolic under partial evaluation — surfaces as
// UnknownValueErr. The two cases are indistinguishable.
//
// When true, the source opts into the same save-set-aware resolver the
// built-in rule indexer uses: a reference that is unknown under partial
// evaluation returns UnknownValueErr, while a reference that is simply
// absent from an otherwise-concrete input resolves to (nil, nil). This lets
// a source tell "deliberately symbolic" apart from "concretely missing"
// on a per-reference basis (e.g. input.foo unknown while input.bar is
// known). See ValueResolver and IsUnknownValueErr.
DistinguishAbsentFromUnknown bool
}
// LookupOption is a functional option for ExternalRuleIndex.Lookup calls.
@@ -79,6 +131,7 @@ type LookupOptions struct {
resolver ValueResolver
requestMetadata map[string]any
responseMetadata map[string]any
params []Value
}
// Metrics returns the metrics instance from the options, or nil if not set.
@@ -101,6 +154,14 @@ func (o *LookupOptions) ResponseMetadata() map[string]any {
return o.responseMetadata
}
// Params returns the parameter values for a parametrized external source (see
// ParametrizedExternalRuleIndex). The slice holds the ground key values that
// followed the registered prefix in the query reference, in order. It is empty
// for conventional (non-parametrized) sources.
func (o *LookupOptions) Params() []Value {
return o.params
}
// LookupMetrics returns a LookupOption that sets the metrics instance
// for the Lookup call.
func LookupMetrics(m metrics.Metrics) LookupOption {
@@ -126,3 +187,11 @@ func LookupResponseMetadata(m map[string]any) LookupOption {
opts.responseMetadata = m
}
}
// LookupParams returns a LookupOption that sets the parameter values handed to a
// parametrized external source (see ParametrizedExternalRuleIndex).
func LookupParams(params []Value) LookupOption {
return func(opts *LookupOptions) {
opts.params = params
}
}

View File

@@ -6,61 +6,12 @@ package ast
import (
"slices"
"sort"
"strings"
"sync"
"github.com/open-policy-agent/opa/v1/util"
)
// RuleIndex defines the interface for rule indices.
type RuleIndex interface {
// Build tries to construct an index for the given rules. If the index was
// constructed, it returns true, otherwise false.
Build(rules []*Rule) bool
// Lookup searches the index for rules that will match the provided
// resolver. If the resolver returns an error, it is returned via err.
Lookup(resolver ValueResolver) (*IndexResult, error)
// AllRules traverses the index and returns all rules that will match
// the provided resolver without any optimizations (effectively with
// indexing disabled). If the resolver returns an error, it is returned
// via err.
AllRules(resolver ValueResolver) (*IndexResult, error)
}
// IndexResult contains the result of an index lookup.
type IndexResult struct {
Rules []*Rule
Else map[*Rule][]*Rule
Default *Rule
Kind RuleKind
EarlyExit bool
OnlyGroundRefs bool
}
// NewIndexResult returns a new IndexResult object.
func NewIndexResult(kind RuleKind) *IndexResult {
return &IndexResult{
Kind: kind,
}
}
// Empty returns true if there are no rules to evaluate.
func (ir *IndexResult) Empty() bool {
return len(ir.Rules) == 0 && ir.Default == nil
}
type baseDocEqIndex struct {
isVirtual func(Ref) bool
root *trieNode
defaultRule *Rule
kind RuleKind
onlyGroundRefs bool
}
var (
equalityRef = Equality.Ref()
equalRef = Equal.Ref()
@@ -69,9 +20,59 @@ var (
internalTestCaseRef = InternalTestCase.Ref()
internalMemberRef = Member.Ref()
globwildcard = VarTerm("$globwildcard")
skipIndexing = NewSet(NewTerm(internalPrintRef), NewTerm(internalTestCaseRef))
// anyValue is a fake variable we used to put "naked ref" expressions
// into the rule index
anyValue Value = Var("__any__")
)
type (
// RuleIndex defines the interface for rule indices.
RuleIndex interface {
// Build tries to construct an index for the given rules. If the index was
// constructed, it returns true, otherwise false.
Build(rules []*Rule) bool
// Lookup searches the index for rules that will match the provided
// resolver. If the resolver returns an error, it is returned via err.
Lookup(resolver ValueResolver) (*IndexResult, error)
// AllRules traverses the index and returns all rules that will match
// the provided resolver without any optimizations (effectively with
// indexing disabled). If the resolver returns an error, it is returned
// via err.
AllRules(resolver ValueResolver) (*IndexResult, error)
}
// IndexResult contains the result of an index lookup.
IndexResult struct {
Rules []*Rule
Else map[*Rule][]*Rule
Default *Rule
Kind RuleKind
EarlyExit bool
OnlyGroundRefs bool
}
baseDocEqIndex struct {
isVirtual func(Ref) bool
root *trieNode
defaultRule *Rule
kind RuleKind
onlyGroundRefs bool
}
)
// NewIndexResult returns a new IndexResult object.
func NewIndexResult(kind RuleKind) *IndexResult {
return &IndexResult{Kind: kind}
}
// Empty returns true if there are no rules to evaluate.
func (ir *IndexResult) Empty() bool {
return len(ir.Rules) == 0 && ir.Default == nil
}
func newBaseDocEqIndex(isVirtual func(Ref) bool) *baseDocEqIndex {
return &baseDocEqIndex{
isVirtual: isVirtual,
@@ -99,14 +100,7 @@ func (i *baseDocEqIndex) Build(rules []*Rule) bool {
if i.onlyGroundRefs {
i.onlyGroundRefs = rule.Head.Reference.IsGround()
}
var skip bool
for i := range rule.Body {
if op := rule.Body[i].OperatorTerm(); op != nil && skipIndexing.Contains(op) {
skip = true
break
}
}
if !skip {
if !slices.ContainsFunc(rule.Body, skipIndexingOperator) {
clear(values)
for i := range rule.Body {
indices.Update(rule, rule.Body[i], values)
@@ -137,15 +131,7 @@ func (i *baseDocEqIndex) Build(rules []*Rule) bool {
} else if len(values) == 1 {
node = node.Insert(ref, values[0].Value, values[0].Mapper)
} else {
var hasVar bool
for i := range values {
if _, isVar := values[i].Value.(Var); isVar {
hasVar = true
break
}
}
if hasVar {
if slices.ContainsFunc(values, (*refindex).isVar) {
child := node.Insert(ref, anyValue, values[0].Mapper)
for i := range values {
if values[i].Mapper != nil {
@@ -183,7 +169,12 @@ func (i *baseDocEqIndex) Lookup(resolver ValueResolver) (*IndexResult, error) {
tr := ttrPool.Get().(*trieTraversalResult)
defer func() {
clear(tr.unordered)
// Note(anderseknert): `clear`ing the map is not good enough here, as it'd mean
// resetting each of its slice values, costing us new allocations on each append
// in subsequent lookups
for i := range tr.unordered {
tr.unordered[i] = tr.unordered[i][:0]
}
tr.ordering = tr.ordering[:0]
tr.multiple = false
tr.exist = nil
@@ -211,9 +202,10 @@ func (i *baseDocEqIndex) Lookup(resolver ValueResolver) (*IndexResult, error) {
clear(result.Else)
for _, pos := range tr.ordering {
slices.SortFunc(tr.unordered[pos], func(a, b *ruleNode) int {
return a.prio[1] - b.prio[1]
})
if len(tr.unordered[pos]) == 0 {
continue
}
slices.SortFunc(tr.unordered[pos], (*ruleNode).prio1Cmp)
nodes := tr.unordered[pos]
root := nodes[0].rule
@@ -267,9 +259,10 @@ func (i *baseDocEqIndex) AllRules(ValueResolver) (*IndexResult, error) {
result.Rules = make([]*Rule, 0, len(tr.ordering))
for _, pos := range tr.ordering {
slices.SortFunc(tr.unordered[pos], func(a, b *ruleNode) int {
return a.prio[1] - b.prio[1]
})
if len(tr.unordered[pos]) == 0 {
continue
}
slices.SortFunc(tr.unordered[pos], (*ruleNode).prio1Cmp)
nodes := tr.unordered[pos]
root := nodes[0].rule
result.Rules = append(result.Rules, root)
@@ -326,15 +319,15 @@ func newrefindices(isVirtual func(Ref) bool) *refindices {
}
}
// anyValue is a fake variable we used to put "naked ref" expressions
// into the rule index
var anyValue = Var("__any__")
func (i *refindex) isVar() bool {
_, isVar := i.Value.(Var)
return isVar
}
// Update attempts to update the refindices for the given expression in the
// given rule. If the expression cannot be indexed the update does not affect
// the indices.
func (i *refindices) Update(rule *Rule, expr *Expr, values map[Var]Value) {
if len(expr.With) > 0 {
// NOTE(tsandall): In the future, we may need to consider expressions
// that have with statements applied to them.
@@ -354,8 +347,8 @@ func (i *refindices) Update(rule *Rule, expr *Expr, values map[Var]Value) {
// check for type "Var" here. But since it's impossible to call a
// function with a undefined argument, there's no point to recording
// "needs to be anything" for function args
if ref, ok := ts.Value.(Ref); ok { // "naked ref"
i.updateEq(rule, ref, anyValue, nil)
if _, ok := ts.Value.(Ref); ok { // "naked ref"
i.updateEq(rule, ts.Value, anyValue, nil)
}
}
}
@@ -397,27 +390,18 @@ func (i *refindices) isValidIndexRef(ref Ref) bool {
// References that appear more frequently in the indexed rules are ordered
// before less frequently appearing references.
func (i *refindices) Sorted() []Ref {
if i.sorted == nil {
counts := make([]int, 0, i.frequency.Len())
i.sorted = make([]Ref, 0, i.frequency.Len())
i.frequency.Iter(func(k Ref, v int) bool {
counts = append(counts, v)
i.sorted = append(i.sorted, k)
return false
})
sort.Slice(i.sorted, func(a, b int) bool {
if counts[a] > counts[b] {
return true
} else if counts[b] > counts[a] {
return false
i.sorted = util.SortedFunc(i.frequency.Keys(), func(a, b Ref) int {
countsA, _ := i.frequency.Get(a)
countsB, _ := i.frequency.Get(b)
if countsA < countsB { // descending, we want highest-freq first
return 1
} else if countsA > countsB {
return -1
}
return i.sorted[a][0].Loc().Compare(i.sorted[b][0].Loc()) < 0
return a[0].Loc().Compare(b[0].Loc())
})
}
return i.sorted
}
@@ -515,12 +499,10 @@ func (i *refindices) updateGlobMatch(rule *Rule, expr *Expr) {
}
func (i *refindices) updateMember(rule *Rule, expr *Expr, constants map[Var]Value) {
args := rule.Head.Args
lhs, rhs := expr.Operand(0), expr.Operand(1)
lvar, ok := lhs.Value.(Var)
if ok {
lref := resolveVarToRef(i.rules[rule], args, lvar)
lref := resolveVarToRef(i.rules[rule], rule.Head.Args, lvar)
if lref != nil {
i.updateMemberRefInValue(rule, lref, rhs, constants) // `ref in value`
return
@@ -528,7 +510,7 @@ func (i *refindices) updateMember(rule *Rule, expr *Expr, constants map[Var]Valu
}
// `var0 in var1` case (var0 may be constant, var1 ref)
i.updateMemberValueInRef(rule, args, lhs.Value, rhs, constants)
i.updateMemberValueInRef(rule, rule.Head.Args, lhs.Value, rhs, constants)
}
func (i *refindices) updateMemberValueInRef(rule *Rule, args []*Term, lval Value, rhs *Term, constants map[Var]Value) {
@@ -615,12 +597,12 @@ func (i *refindices) resolveAndValidateRef(rule *Rule, args []*Term, term *Term)
// as we're not capturing `var = var` expressions in the index.
func resolveVarToRef(ri []*refindex, args []*Term, v Var) Ref {
for _, other := range ri {
if ov, ok := other.Value.(Var); ok && ov.Equal(v) {
if v.Equal(other.Value) {
return other.Ref
}
}
for j, arg := range args {
if arg.Value.Compare(v) == 0 {
if v.Equal(arg.Value) {
return Ref{FunctionArgRootDocument, InternedTerm(j)}
}
}
@@ -636,7 +618,6 @@ func (i *refindices) insert(rule *Rule, index *refindex) {
for pos, other := range i.rules[rule] {
if other.Ref.Equal(index.Ref) {
if ValueEqual(other.Value, index.Value) {
return
}
@@ -671,7 +652,7 @@ type trieTraversalResult struct {
multiple bool
}
var ttrPool = sync.Pool{
var ttrPool = &sync.Pool{
New: func() any {
return newTrieTraversalResult()
},
@@ -679,21 +660,17 @@ var ttrPool = sync.Pool{
func newTrieTraversalResult() *trieTraversalResult {
return &trieTraversalResult{
unordered: map[int][]*ruleNode{},
unordered: make(map[int][]*ruleNode, 16),
}
}
func (tr *trieTraversalResult) Add(t *trieNode) {
for _, node := range t.rules {
root := node.prio[0]
nodes, ok := tr.unordered[root]
if !ok {
if nodes, ok := tr.unordered[root]; !ok || len(nodes) == 0 {
tr.ordering = append(tr.ordering, root)
}
// Deduplicate: check if a ruleNode with this priority already exists
if !slices.ContainsFunc(nodes, func(existing *ruleNode) bool {
return existing.prio == node.prio
}) {
tr.unordered[root] = append(nodes, node)
} else if !slices.ContainsFunc(nodes, node.prioEqual) {
tr.unordered[root] = append(nodes, node)
}
}
@@ -740,10 +717,16 @@ type ruleNode struct {
rule *Rule
}
func (a *ruleNode) prio1Cmp(b *ruleNode) int {
return a.prio[1] - b.prio[1]
}
func (a *ruleNode) prioEqual(b *ruleNode) bool {
return a.prio == b.prio
}
func newTrieNodeImpl() *trieNode {
return &trieNode{
scalars: util.NewHasherMap[Value, *trieNode](ValueEqual),
}
return &trieNode{}
}
func (node *trieNode) Do(walker trieWalker) {
@@ -768,7 +751,6 @@ func (node *trieNode) Do(walker trieWalker) {
}
func (node *trieNode) Insert(ref Ref, value Value, mapper *valueMapper) *trieNode {
if node.next == nil {
node.next = newTrieNodeImpl()
node.next.ref = ref
@@ -782,7 +764,6 @@ func (node *trieNode) Insert(ref Ref, value Value, mapper *valueMapper) *trieNod
}
func (node *trieNode) Traverse(resolver ValueResolver, tr *trieTraversalResult) error {
if node == nil {
return nil
}
@@ -802,54 +783,70 @@ func (node *trieNode) addMapper(mapper *valueMapper) {
}
func (node *trieNode) insertValue(value Value) *trieNode {
switch value := value.(type) {
case nil:
if node.undefined == nil {
node.undefined = newTrieNodeImpl()
}
node.undefined = util.Or(node.undefined, newTrieNodeImpl)
return node.undefined
case Var:
if node.any == nil {
node.any = newTrieNodeImpl()
}
node.any = util.Or(node.any, newTrieNodeImpl)
return node.any
case Null, Boolean, Number, String:
child, ok := node.scalars.Get(value)
if !ok {
child = newTrieNodeImpl()
if node.scalars == nil {
node.scalars = util.NewHasherMap[Value, *trieNode](ValueEqual)
}
node.scalars.Put(value, child)
}
return child
case *Array:
if node.array == nil {
node.array = newTrieNodeImpl()
}
node.array = util.Or(node.array, newTrieNodeImpl)
return node.array.insertArray(value)
// `x in <collection>` (see updateMemberRefInValue) inserts each element of
// the literal collection as-is, without restricting it to scalars/arrays
// like the equality-based indexing does (see indexValue). A ground
// Object or Set element can't be indexed precisely, so - like Var - it
// falls back to the "any" node: the rule stays a candidate for every
// input value. (The other composite Value types - Ref, comprehensions,
// Call - can't actually reach here: the compiler rewrites them into
// separate statements, bound to a Var, before the index is built.)
case Object, Set:
node.any = util.Or(node.any, newTrieNodeImpl)
return node.any
}
panic("illegal value")
}
func (node *trieNode) insertArray(arr *Array) *trieNode {
if arr.Len() == 0 {
return node
}
switch head := arr.Elem(0).Value.(type) {
case Var:
if node.any == nil {
node.any = newTrieNodeImpl()
}
node.any = util.Or(node.any, newTrieNodeImpl)
return node.any.insertArray(arr.Slice(1, -1))
case Null, Boolean, Number, String:
child, ok := node.scalars.Get(head)
if !ok {
child = newTrieNodeImpl()
if node.scalars == nil {
node.scalars = util.NewHasherMap[Value, *trieNode](ValueEqual)
}
node.scalars.Put(head, child)
}
return child.insertArray(arr.Slice(1, -1))
// Same reasoning as in insertValue above: an array element can itself be
// a nested array, object, or set, none of which can be indexed precisely
// at this position, so fall back to "any" and keep indexing the
// remaining elements.
case *Array, Object, Set:
node.any = util.Or(node.any, newTrieNodeImpl)
return node.any.insertArray(arr.Slice(1, -1))
}
panic("illegal value")
@@ -868,8 +865,7 @@ func (node *trieNode) traverse(resolver ValueResolver, tr *trieTraversalResult)
return err
}
err = node.undefined.Traverse(resolver, tr)
if err != nil {
if err = node.undefined.Traverse(resolver, tr); err != nil {
return err
}
@@ -877,13 +873,11 @@ func (node *trieNode) traverse(resolver ValueResolver, tr *trieTraversalResult)
return nil
}
err = node.any.Traverse(resolver, tr)
if err != nil {
if err = node.any.Traverse(resolver, tr); err != nil {
return err
}
err = node.traverseValue(resolver, tr, v)
if err != nil {
if err = node.traverseValue(resolver, tr, v); err != nil {
return err
}
@@ -900,7 +894,6 @@ func (node *trieNode) traverse(resolver ValueResolver, tr *trieTraversalResult)
}
func (node *trieNode) traverseValue(resolver ValueResolver, tr *trieTraversalResult, value Value) error {
switch value := value.(type) {
case *Array, Set, Object:
if node.array != nil {
@@ -910,19 +903,13 @@ func (node *trieNode) traverseValue(resolver ValueResolver, tr *trieTraversalRes
}
}
}
if node.scalars.Len() > 0 {
return node.traverseCollectionMembership(resolver, tr, value)
}
return nil
case Null, Boolean, Number, String:
child, ok := node.scalars.Get(value)
if !ok {
return nil
if child, ok := node.scalars.Get(value); ok {
return child.Traverse(resolver, tr)
}
return child.Traverse(resolver, tr)
}
return nil
@@ -951,7 +938,7 @@ func (node *trieNode) traverseCollectionMembership(resolver ValueResolver, tr *t
return nil
}
func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalResult, arr *Array) error {
func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalResult, arr *Array) (err error) {
if node == nil {
return nil
}
@@ -960,24 +947,15 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
return node.Traverse(resolver, tr)
}
err := node.any.traverseArray(resolver, tr, arr.Slice(1, -1))
if err != nil {
return err
if err = node.any.traverseArray(resolver, tr, arr.Slice(1, -1)); err == nil {
switch head := arr.Elem(0).Value.(type) {
case Null, Boolean, Number, String:
child, _ := node.scalars.Get(head)
return child.traverseArray(resolver, tr, arr.Slice(1, -1))
}
}
head := arr.Elem(0).Value
if !IsScalar(head) {
return nil
}
switch head := head.(type) {
case Null, Boolean, Number, String:
child, _ := node.scalars.Get(head)
return child.traverseArray(resolver, tr, arr.Slice(1, -1))
}
panic("illegal value")
return err
}
func (node *trieNode) traverseUnknown(resolver ValueResolver, tr *trieTraversalResult) error {
@@ -1105,10 +1083,7 @@ func globDelimiterToString(delim *Term) (string, bool) {
return result, true
}
var globwildcard = VarTerm("$globwildcard")
func globPatternToArray(pattern *Term, delim string) *Term {
s, ok := pattern.Value.(String)
if !ok {
return nil
@@ -1147,7 +1122,6 @@ func globPatternToArray(pattern *Term, delim string) *Term {
// splits s on characters in delim except if delim characters have been escaped
// with reverse solidus.
func splitStringEscaped(s string, delim string) []string {
var last, curr int
var escaped bool
var result []string
@@ -1171,7 +1145,12 @@ func splitStringEscaped(s string, delim string) []string {
func stringSliceToArray(s []string) *Array {
arr := make([]*Term, len(s))
for i, v := range s {
arr[i] = StringTerm(v)
arr[i] = InternedTerm(v)
}
return NewArray(arr...)
}
func skipIndexingOperator(expr *Expr) bool {
op := expr.OperatorTerm()
return op != nil && skipIndexing.Contains(op)
}

View File

@@ -3,12 +3,10 @@ package location
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"unicode/utf8"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
@@ -150,41 +148,3 @@ func (loc *Location) Compare(other *Location) int {
}
return 0
}
func (loc *Location) MarshalJSON() ([]byte, error) {
// structs are used here to preserve the field ordering of the original Location struct
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.ExcludeLocationFile {
data := struct {
Row int `json:"row"`
Col int `json:"col"`
Text []byte `json:"text,omitempty"`
}{
Row: loc.Row,
Col: loc.Col,
}
if jsonOptions.IncludeLocationText {
data.Text = loc.Text
}
return json.Marshal(data)
}
data := struct {
File string `json:"file"`
Row int `json:"row"`
Col int `json:"col"`
Text []byte `json:"text,omitempty"`
}{
Row: loc.Row,
Col: loc.Col,
File: loc.File,
}
if jsonOptions.IncludeLocationText {
data.Text = loc.Text
}
return json.Marshal(data)
}

View File

@@ -0,0 +1,47 @@
//go:build !go1.27
package location
import (
"encoding/json"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
)
func (loc *Location) MarshalJSON() ([]byte, error) {
// structs are used here to preserve the field ordering of the original Location struct
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.ExcludeLocationFile {
data := struct {
Row int `json:"row"`
Col int `json:"col"`
Text []byte `json:"text,omitempty"`
}{
Row: loc.Row,
Col: loc.Col,
}
if jsonOptions.IncludeLocationText {
data.Text = loc.Text
}
return json.Marshal(data)
}
data := struct {
File string `json:"file"`
Row int `json:"row"`
Col int `json:"col"`
Text []byte `json:"text,omitempty"`
}{
Row: loc.Row,
Col: loc.Col,
File: loc.File,
}
if jsonOptions.IncludeLocationText {
data.Text = loc.Text
}
return json.Marshal(data)
}

View File

@@ -0,0 +1,50 @@
// 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.
//go:build go1.27
package location
import (
"encoding/base64"
"encoding/json/jsontext"
"encoding/json/v2"
"github.com/open-policy-agent/opa/internal/jsonv2"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
)
// Location is an exported type, so losing MarshalJSON here would be a
// breaking API change even though callers should go through json.Marshal,
// not this method directly.
var _ json.Marshaler = &Location{}
// MarshalJSON returns the JSON encoding of loc.
func (loc *Location) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(loc)
}
func (loc *Location) MarshalJSONTo(e *jsontext.Encoder) (err error) {
e.WriteToken(jsontext.BeginObject)
jsonOptions := astJSON.GetOptions().MarshalOptions
if !jsonOptions.ExcludeLocationFile {
e.WriteToken(jsontext.String("file"))
e.WriteToken(jsontext.String(loc.File))
}
e.WriteToken(jsontext.String("row"))
e.WriteToken(jsontext.Int(int64(loc.Row)))
e.WriteToken(jsontext.String("col"))
e.WriteToken(jsontext.Int(int64(loc.Col)))
// NOTE: len check to match the `json:"text,omitempty"` behaviour of the
// pre-go1.27 marshaller.
if jsonOptions.IncludeLocationText && len(loc.Text) > 0 {
e.WriteToken(jsontext.String("text"))
e.WriteToken(jsontext.String(base64.StdEncoding.EncodeToString(loc.Text)))
}
return e.WriteToken(jsontext.EndObject)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/open-policy-agent/opa/v1/util"
)
// ValueMap represents a key/value map between AST term values. Any type of term
// ValueMap represents a key/value map between AST term values. Any type of value
// can be used as a key in the map.
type ValueMap struct {
hashMap *util.TypedHashMap[Value, Value]

View File

@@ -152,7 +152,7 @@ func (rule *Rule) mermaidFormat(b *mermaidBuilder) string {
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range rule.Body {
exprID := mermaidFormatExpr(expr, i, b)
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
}
@@ -188,7 +188,7 @@ func mermaidFormatHead(head *Head, b *mermaidBuilder) string {
return id
}
func mermaidFormatExpr(expr *Expr, index int, b *mermaidBuilder) string {
func mermaidFormatExpr(expr *Expr, b *mermaidBuilder) string {
label := expr.String()
id := b.node("hex", label)
@@ -245,8 +245,8 @@ func mermaidFormatEvery(every *Every, b *mermaidBuilder) string {
b.edgeLabeled(id, domainID, "domain")
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range every.Body {
exprID := mermaidFormatExpr(expr, i, b)
for _, expr := range every.Body {
exprID := mermaidFormatExpr(expr, b)
b.edge(bodyID, exprID)
}
return id
@@ -256,14 +256,14 @@ func mermaidFormatLogical(op string, lhs, rhs Body, b *mermaidBuilder) string {
id := b.node("rect", op)
lhsID := b.node("rect", "Lhs")
b.edge(id, lhsID)
for i, expr := range lhs {
exprID := mermaidFormatExpr(expr, i, b)
for _, expr := range lhs {
exprID := mermaidFormatExpr(expr, b)
b.edge(lhsID, exprID)
}
rhsID := b.node("rect", "Rhs")
b.edge(id, rhsID)
for i, expr := range rhs {
exprID := mermaidFormatExpr(expr, i, b)
for _, expr := range rhs {
exprID := mermaidFormatExpr(expr, b)
b.edge(rhsID, exprID)
}
return id
@@ -283,7 +283,7 @@ func mermaidFormatWith(w *With, b *mermaidBuilder) string {
func (not *Not) mermaidFormat(b *mermaidBuilder) string {
id := b.node("stadium", "not")
for i, expr := range not.Body {
exprID := mermaidFormatExpr(expr, i, b)
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(id, exprID, strconv.Itoa(i))
}
return id
@@ -381,7 +381,7 @@ func (ac *ArrayComprehension) mermaidFormat(b *mermaidBuilder) string {
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range ac.Body {
exprID := mermaidFormatExpr(expr, i, b)
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id
@@ -396,7 +396,7 @@ func (oc *ObjectComprehension) mermaidFormat(b *mermaidBuilder) string {
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range oc.Body {
exprID := mermaidFormatExpr(expr, i, b)
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id
@@ -409,7 +409,7 @@ func (sc *SetComprehension) mermaidFormat(b *mermaidBuilder) string {
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range sc.Body {
exprID := mermaidFormatExpr(expr, i, b)
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id

View File

@@ -764,15 +764,15 @@ func (p *Parser) parseImport() *Import {
t := r[0]
name := string(t.Value.(Var))
if IsKeywordInRegoVersion(name, p.po.EffectiveRegoVersion()) {
p.errorf(t.Location, "unexpected import path, must not end with a keyword, got: %s", name)
p.hint("import a different path or use an alias")
p.errorf(t.Location, "unexpected import path, must not end with a keyword, got: %s", name)
}
} else if !FutureRootDocument.Equal(r[0]) {
t := r[len(r)-1]
name := string(t.Value.(String))
if IsKeywordInRegoVersion(name, p.po.EffectiveRegoVersion()) {
p.errorf(t.Location, "unexpected import path, must not end with a keyword, got: %s", name)
p.hint("import a different path or use an alias")
p.errorf(t.Location, "unexpected import path, must not end with a keyword, got: %s", name)
}
}
@@ -1246,17 +1246,25 @@ func (p *Parser) parseLiteral() (expr *Expr) {
if outer == nil {
return nil
}
if p.s.tok == tokens.With {
if outer.With = p.parseWith(); outer.With == nil {
return nil
}
}
return outer
return p.attachWith(outer)
}
}
p.restore(s)
}
// LHS/whole parenthesized group at statement start: `(a or b)`,
// `(a or b) and c`, or `({a}) and c`. parseLogicalGroup only commits when the
// parens hold or precede an and/or; otherwise (`({})`, `({a})`, `(a == b)`) it
// restores and we fall through so parseExpr handles the term.
if p.s.tok == tokens.LParen && p.logicalKeywordsActive() {
if body, explicit, loc, committed := p.parseLogicalGroup(false); committed {
if body == nil {
return nil
}
return p.foldLogicalTail(body, explicit, loc)
}
}
// Check that we're not parsing a ref
if p.isAllowedRefKeyword(p.s.tok) {
// Scan ahead
@@ -1280,14 +1288,10 @@ func (p *Parser) parseLiteral() (expr *Expr) {
if negated && p.notBodies && p.s.tok == tokens.LBrace {
nb := p.parseNotBody(notLoc)
if nb != nil && p.s.tok == tokens.With {
if nb.With = p.parseWith(); nb.With == nil {
return nil
}
if nb == nil {
return nil
}
return nb
return p.attachWith(nb)
}
switch p.s.tok {
@@ -1324,6 +1328,22 @@ func (p *Parser) parseLiteralExpr(negated bool, notLoc *Location) *Expr {
startOffset := p.s.loc.Offset
startLoc := p.s.Loc()
s := p.save()
// Negated parenthesized group: `not (a or b)`. The parens are an operand of
// `not`, so any `{...}` inside is a body.
if negated && p.notBodies && p.s.tok == tokens.LParen && p.logicalKeywordsActive() {
if body, explicit, _, committed := p.parseLogicalGroup(true); committed {
if body == nil {
return nil
}
spanned := p.extendLoc(notLoc)
not := NewExpr(&Not{Body: body, ExplicitBody: explicit, Location: spanned}).SetLocation(spanned)
return p.foldLogicalTail(NewBody(not), false, spanned)
}
}
expr := p.parseExpr()
if expr != nil {
var withLoc *Location
@@ -1370,10 +1390,7 @@ func (p *Parser) parseLiteralExpr(negated bool, notLoc *Location) *Expr {
if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
if withLoc != nil {
kw := p.s.tok.String()
p.errorf(withLoc,
"`with` modifier is not allowed on operand of `%s`; wrap the operand in `{...}` to scope, or move `with` after the %s expression to apply it to the whole expression",
kw, kw)
p.errWithOnOperand(withLoc, p.s.tok.String())
return nil
}
@@ -1386,12 +1403,7 @@ func (p *Parser) parseLiteralExpr(negated bool, notLoc *Location) *Expr {
if outer == nil {
return nil
}
if p.s.tok == tokens.With {
if outer.With = p.parseWith(); outer.With == nil {
return nil
}
}
return outer
return p.attachWith(outer)
}
}
return expr
@@ -1445,6 +1457,35 @@ func (p *Parser) parseWith() []*With {
return withs
}
func (p *Parser) attachWith(e *Expr) *Expr {
if e != nil && p.s.tok == tokens.With {
if e.With = p.parseWith(); e.With == nil {
return nil
}
}
return e
}
func (p *Parser) errWithOnOperand(loc *Location, kw string) {
p.hint(fmt.Sprintf(
"Wrap the operand in `(...)` or `{...}` to scope, or move `with` after the `%s` expression to apply it to the whole expression",
kw))
p.errorf(loc,
"`with` modifier is not allowed on operand of `%s`",
kw)
}
func (p *Parser) foldLogicalTail(body Body, explicit bool, loc *Location) *Expr {
if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
outer := p.parseLogicalOrChain(body, explicit, loc)
if outer == nil {
return nil
}
return p.attachWith(outer)
}
return p.attachWith(body[0])
}
func (p *Parser) parseSome() *Expr {
decl := &SomeDecl{}
@@ -1692,6 +1733,26 @@ func (p *Parser) parseLogicalOperand() (Body, bool, *Location) {
return NewBody(nb), false, nb.Location
}
// Parenthesized logical group operand: `(a or b)` or, when negated,
// `not (a or b)`. This is an operand of and/or/not, so a `{...}` inside is a
// body. If the parens don't hold a logical group parseLogicalGroup restores
// state and we fall through so parseExpr can handle `(a == b)` as a term.
if p.s.tok == tokens.LParen && p.logicalKeywordsActive() && (!negated || p.notBodies) {
if body, explicit, loc, committed := p.parseLogicalGroup(true); committed {
if body == nil {
return nil, false, nil
}
if negated {
spanned := p.extendLoc(notLoc)
not := NewExpr(&Not{Body: body, ExplicitBody: explicit, Location: spanned}).SetLocation(spanned)
return NewBody(not), false, spanned
}
return body, explicit, loc
}
}
startOffset := p.s.loc.Offset
startLoc := p.s.Loc()
expr := p.parseExpr()
@@ -1716,6 +1777,161 @@ func (p *Parser) parseLogicalOperand() (Body, bool, *Location) {
return NewBody(expr), false, expr.Location
}
// isLogicalBody reports whether b is a single-expression body wrapping a
// LogicalAnd/LogicalOr node, i.e. the result of a parenthesized or nested group.
func isLogicalBody(b Body) bool {
if len(b) != 1 {
return false
}
switch b[0].Terms.(type) {
case *LogicalAnd, *LogicalOr:
return true
}
return false
}
// isNegatedOperand reports whether b is a single negated operand, e.g. `not a`
// (either a *Not node or an expression with Negated set).
func isNegatedOperand(b Body) bool {
if len(b) != 1 {
return false
}
if b[0].Negated {
return true
}
_, ok := b[0].Terms.(*Not)
return ok
}
// expectRParen consumes the closing `)` of a group, reporting an error if the
// current token is not `)`.
func (p *Parser) expectRParen() bool {
if p.s.tok != tokens.RParen {
p.error(p.s.Loc(), "expected ) to close parenthesized group")
return false
}
p.scan()
return true
}
// parseLogicalGroup attempts to parse a parenthesized grouping of `and`/`or`/`not`
// operands starting at the current `(`.
//
// operandContext reports whether the `(` is already an operand of `and`/`or`/`not`.
func (p *Parser) parseLogicalGroup(operandContext bool) (Body, bool, *Location, bool) {
if !p.enter() {
return nil, false, nil, true
}
defer p.leave()
s := p.save()
openLoc := p.s.Loc()
p.scan() // consume `(`
if p.s.tok == tokens.RParen {
if operandContext {
p.error(openLoc, "empty parenthesized group")
return nil, false, nil, true
}
p.restore(s)
return nil, false, nil, false
}
// A leading `{` is a body only in an operand context; otherwise it's an
// object/set literal and we backtrack to the term parser.
braceLead := p.s.tok == tokens.LBrace
lhsBody, lhsExplicit, lhsLoc := p.parseLogicalOperand()
if lhsBody == nil {
// An empty `{}` operand (e.g. `not ({})`) is a body error.
if operandContext && braceLead {
return nil, false, nil, true
}
p.restore(s)
return nil, false, nil, false
}
switch {
case p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr:
expr := p.parseLogicalOrChain(lhsBody, lhsExplicit, lhsLoc)
if expr == nil {
return nil, false, nil, true
}
// A trailing `with` binds to the whole group, e.g. `(a and b with x)`.
if expr = p.attachWith(expr); expr == nil {
return nil, false, nil, true
}
if !p.expectRParen() {
return nil, false, nil, true
}
return NewBody(expr), false, p.extendLoc(openLoc), true
case p.s.tok == tokens.With && !lhsExplicit && len(lhsBody) == 1:
// Single-operand group carrying a `with`, e.g. `(a with x)`; the `with`
// binds to the sole operand.
withLoc := p.s.Loc()
if p.attachWith(lhsBody[0]) == nil {
return nil, false, nil, true
}
// A `with` on the operand followed by `and`/`or` is ambiguous.
if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
p.errWithOnOperand(withLoc, p.s.tok.String())
return nil, false, nil, true
}
if !p.expectRParen() {
return nil, false, nil, true
}
if operandContext || p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
return lhsBody, false, p.extendLoc(openLoc), true
}
p.restore(s)
return nil, false, nil, false
case lhsExplicit:
// `({ body })`
if !p.expectRParen() {
return nil, false, nil, true
}
if operandContext || p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
return lhsBody, true, p.extendLoc(openLoc), true
}
p.restore(s)
return nil, false, nil, false
case isLogicalBody(lhsBody):
// `(( ... ))`; redundant parens around a nested group.
if !p.expectRParen() {
return nil, false, nil, true
}
return lhsBody, false, p.extendLoc(openLoc), true
case isNegatedOperand(lhsBody):
// `(not ...)`
if !p.expectRParen() {
return nil, false, nil, true
}
return lhsBody, false, p.extendLoc(openLoc), true
default:
// Single non-logical operand, e.g. `(a == b)`: not a group.
p.restore(s)
return nil, false, nil, false
}
}
func (p *Parser) parseEvery() *Expr {
qb := &Every{}
qb.SetLoc(p.s.Loc())
@@ -3081,6 +3297,16 @@ func (b *metadataParser) Append(c *Comment) {
var yamlLineErrRegex = regexp.MustCompile(`^yaml:(?: unmarshal errors:[\n\s]*)? line ([[:digit:]]+):`)
// endLoc returns the location of the last comment in the METADATA block, or nil
// if there are none. Only this location is retained on Annotations (for
// EndLoc), so the comment slice itself is never aliased onto the result.
func endLoc(comments []*Comment) *location.Location {
if len(comments) == 0 {
return nil
}
return comments[len(comments)-1].Location
}
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")
@@ -3091,8 +3317,7 @@ func (b *metadataParser) Parse() (result *Annotations, err error) {
var comment *Comment
match := yamlLineErrRegex.FindStringSubmatch(err.Error())
if len(match) == 2 {
index, err2 := strconv.Atoi(match[1])
if err2 == nil {
if index, ok := util.Atoi(match[1]); ok {
if index >= len(b.comments) {
comment = b.comments[len(b.comments)-1]
} else {
@@ -3110,7 +3335,11 @@ func (b *metadataParser) Parse() (result *Annotations, err error) {
}
result = &Annotations{
comments: b.comments,
// NOTE: only the last comment's location is retained (as endLoc); the
// comment slice itself is backed by a reused buffer (the metadataParser
// is pooled and Reset truncates rather than reallocates), so it must not
// be aliased here.
endLoc: endLoc(b.comments),
Scope: raw.Scope,
Entrypoint: raw.Entrypoint,
Title: raw.Title,
@@ -3173,7 +3402,7 @@ func (b *metadataParser) Parse() (result *Annotations, err error) {
switch v := v.(type) {
case string:
a.Schema, err = parseSchemaRef(v)
a.Schema, err = ParseSchemaRef(v)
if err != nil {
return nil, err
}
@@ -3276,10 +3505,14 @@ func unwrapPair(pair map[string]any) (string, any) {
var errInvalidSchemaRef = errors.New("invalid schema reference")
// ParseSchemaRef parses a schema reference string into a Ref. Unlike
// ParseRef, it accepts the bare `schema` Var and Refs prefixed with the
// schema root document.
//
// NOTE(tsandall): 'schema' is not registered as a root because it's not
// supported by the compiler or evaluator today. Once we fix that, we can remove
// this function.
func parseSchemaRef(s string) (Ref, error) {
func ParseSchemaRef(s string) (Ref, error) {
term, err := ParseTerm(s)
if err == nil {
@@ -3507,9 +3740,9 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
if keyword == "not" {
p.notBodies = true
} else {
kwds = []string{keyword} // overwrite
}
kwds = []string{keyword} // overwrite
}
for _, kw := range kwds {

View File

@@ -6,7 +6,6 @@ package ast
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"strings"
@@ -273,6 +272,11 @@ type (
generatedFrom *Expr
generates []*Expr
// fromAssignment marks an equality expression that was rewritten from
// `:=`, so the safety checker can keep the RHS from being made safe
// through the LHS. See reorderBodyForSafety.
fromAssignment bool
}
// SomeDecl represents a variable declaration statement. The symbols are variables.
@@ -397,33 +401,13 @@ func (mod *Module) String() string {
func (mod *Module) RuleSet(name Var) RuleSet {
rs := NewRuleSet()
for _, rule := range mod.Rules {
if rule.Head.Name.Equal(name) {
if rule.Head.Name == name {
rs.Add(rule)
}
}
return rs
}
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
// will have their module pointer set to mod.
func (mod *Module) UnmarshalJSON(bs []byte) error {
// Declare a new type and use a type conversion to avoid recursively calling
// Module#UnmarshalJSON.
type module Module
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
return err
}
WalkRules(mod, func(rule *Rule) bool {
rule.Module = mod
return false
})
return nil
}
func (mod *Module) regoV1Compatible() bool {
return mod.regoVersion == RegoV1 || mod.regoVersion == RegoV0CompatV1
}
@@ -514,20 +498,6 @@ func (pkg *Package) String() string {
return util.ByteSliceToString(buf)
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": pkg.Path,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package {
if pkg.Location != nil {
data["location"] = pkg.Location
}
}
return json.Marshal(data)
}
// IsValidImportPath returns an error indicating if the import path is invalid.
// If the import path is valid, err is nil.
func IsValidImportPath(v Value) (err error) {
@@ -618,24 +588,6 @@ func (imp *Import) String() string {
return util.ByteSliceToString(buf)
}
func (imp *Import) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": imp.Path,
}
if len(imp.Alias) != 0 {
data["alias"] = imp.Alias
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import {
if imp.Location != nil {
data["location"] = imp.Location
}
}
return json.Marshal(data)
}
// Compare returns an integer indicating whether rule is less than, equal to,
// or greater than other.
func (rule *Rule) Compare(other *Rule) int {
@@ -749,42 +701,6 @@ 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 := ruleJSON{
Head: rule.Head,
Body: rule.Body,
}
if rule.Default {
data.Default = true
}
if rule.Else != nil {
data.Else = rule.Else
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
data.Location = rule.Location
}
if len(rule.Annotations) != 0 {
data.Annotations = rule.Annotations
}
return json.Marshal(data)
}
// 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 {
@@ -947,27 +863,6 @@ func (head *Head) stringWithOpts(opts toStringOpts) string {
return util.ByteSliceToString(buf)
}
func (head *Head) MarshalJSON() ([]byte, error) {
var loc *Location
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && head.Location != nil {
loc = head.Location
}
// NOTE(sr): we do this to override the rendering of `head.Reference`.
// It's still what'll be used via the default means of encoding/json
// for unmarshaling a json object into a Head struct!
type h Head
return json.Marshal(struct {
h
Ref Ref `json:"ref"`
Location *Location `json:"location,omitempty"`
}{
h: h(*head),
Ref: head.Ref(),
Location: loc,
})
}
// Vars returns a set of vars found in the head.
func (head *Head) Vars() VarSet {
vis := NewVarVisitor()
@@ -1046,17 +941,6 @@ func NewBody(exprs ...*Expr) Body {
return Body(exprs)
}
// MarshalJSON returns JSON encoded bytes representing body.
func (body Body) MarshalJSON() ([]byte, error) {
// Serialize empty Body to empty array. This handles both the empty case and the
// nil case (whereas by default the result would be null if body was nil.)
if len(body) == 0 {
return []byte(`[]`), nil
}
ret, err := json.Marshal([]*Expr(body))
return ret, err
}
// Append adds the expr to the body and updates the expr's index accordingly.
func (body *Body) Append(expr *Expr) {
n := len(*body)
@@ -1076,19 +960,7 @@ func (body Body) Set(expr *Expr, pos int) {
//
// If body is a subset of other, it is considered less than (and vice versa).
func (body Body) Compare(other Body) int {
minLen := min(len(other), len(body))
for i := range minLen {
if cmp := body[i].Compare(other[i]); cmp != 0 {
return cmp
}
}
if len(body) < len(other) {
return -1
}
if len(other) < len(body) {
return 1
}
return 0
return slices.CompareFunc(body, other, (*Expr).Compare)
}
// Copy returns a deep copy of body.
@@ -1149,10 +1021,6 @@ func (body Body) String() string {
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
// control which vars are included.
func (body Body) Vars(params VarVisitorParams) VarSet {
@@ -1528,51 +1396,6 @@ func (expr *Expr) String() string {
return util.ByteSliceToString(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 := exprJSON{
Index: expr.Index,
Terms: expr.Terms,
}
if len(expr.With) > 0 {
data.With = expr.With
}
if expr.Generated {
data.Generated = true
}
if expr.Negated {
data.Negated = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
data.Location = expr.Location
}
return json.Marshal(data)
}
// UnmarshalJSON parses the byte array and stores the result in expr.
func (expr *Expr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalExpr(expr, v)
}
// Vars returns a VarSet containing variables in expr. The params can be set to
// control which vars are included.
func (expr *Expr) Vars(params VarVisitorParams) VarSet {
@@ -1589,6 +1412,7 @@ func NewBuiltinExpr(terms ...*Term) *Expr {
func (expr *Expr) CogeneratedExprs() []*Expr {
visited := map[*Expr]struct{}{}
var result []*Expr
visitCogeneratedExprs(expr, func(e *Expr) bool {
if expr.Equal(e) {
return true
@@ -1597,13 +1421,13 @@ func (expr *Expr) CogeneratedExprs() []*Expr {
return true
}
visited[e] = struct{}{}
// Append during visitation so the result order is deterministic; iterating
// the 'visited' map here would randomize the order and, in turn, make
// dependent output (e.g. PrettyEvent's --var-values) nondeterministic.
result = append(result, e)
return false
})
result := make([]*Expr, 0, len(visited))
for e := range visited {
result = append(result, e)
}
return result
}
@@ -1660,20 +1484,6 @@ func (d *SomeDecl) Hash() int {
return termSliceHash(d.Symbols)
}
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
data := map[string]any{
"symbols": d.Symbols,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
if d.Location != nil {
data["location"] = d.Location
}
}
return json.Marshal(data)
}
func (q *Every) String() string {
if q.Key != nil {
return fmt.Sprintf("every %s, %s in %s { %s }",
@@ -1730,23 +1540,6 @@ func (q *Every) KeyValueVars() VarSet {
return vis.vars
}
func (q *Every) MarshalJSON() ([]byte, error) {
data := map[string]any{
"key": q.Key,
"value": q.Value,
"domain": q.Domain,
"body": q.Body,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every {
if q.Location != nil {
data["location"] = q.Location
}
}
return json.Marshal(data)
}
func (a *LogicalAnd) String() string {
return formatBinaryLogical("and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs)
}
@@ -1780,36 +1573,6 @@ func (a *LogicalAnd) Hash() int {
return a.Lhs.Hash() + a.Rhs.Hash()
}
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "and",
"lhs": a.Lhs,
"rhs": a.Rhs,
}
if a.ExplicitLhs {
data["explicit_lhs"] = true
}
if a.ExplicitRhs {
data["explicit_rhs"] = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And {
if a.Location != nil {
data["location"] = a.Location
}
}
return json.Marshal(data)
}
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
}
func (o *LogicalOr) String() string {
return formatBinaryLogical("or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs)
}
@@ -1843,86 +1606,64 @@ func (o *LogicalOr) Hash() int {
return o.Lhs.Hash() + o.Rhs.Hash()
}
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "or",
"lhs": o.Lhs,
"rhs": o.Rhs,
}
if o.ExplicitLhs {
data["explicit_lhs"] = true
}
if o.ExplicitRhs {
data["explicit_rhs"] = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or {
if o.Location != nil {
data["location"] = o.Location
}
}
return json.Marshal(data)
}
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
}
func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error {
lhsRaw, ok := v["lhs"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"])
}
l, err := unmarshalBody(lhsRaw)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err)
}
*lhs = l
rhsRaw, ok := v["rhs"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"])
}
r, err := unmarshalBody(rhsRaw)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err)
}
*rhs = r
if x, ok := v["explicit_lhs"]; ok {
b, ok := x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x)
}
*explicitLhs = b
}
if x, ok := v["explicit_rhs"]; ok {
b, ok := x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x)
}
*explicitRhs = b
}
return nil
}
func formatBinaryLogical(op string, lhs, rhs Body, explicitLhs, explicitRhs bool) string {
return formatLogicalOperand(lhs, explicitLhs) + " " + op + " " + formatLogicalOperand(rhs, explicitRhs)
return formatLogicalOperand(lhs, explicitLhs, op, false) + " " + op + " " + formatLogicalOperand(rhs, explicitRhs, op, true)
}
func formatLogicalOperand(b Body, explicit bool) string {
if explicit {
func formatLogicalOperand(b Body, explicit bool, parentOp string, rhs bool) string {
if explicit || len(b) != 1 {
return "{ " + b.String() + " }"
}
if logicalOperandNeedsParens(b, parentOp, rhs) {
return "(" + b.String() + ")"
}
return b.String()
}
func logicalOperandNeedsParens(b Body, parentOp string, rhs bool) bool {
if len(b) != 1 {
return false
}
e := b[0]
if len(e.With) > 0 {
return true
}
switch e.Terms.(type) {
case *LogicalOr:
// `or` binds looser than `and`: always parenthesize under `and`; under
// `or`, parenthesize only the rhs to preserve right-nesting.
return parentOp == "and" || rhs
case *LogicalAnd:
// `and` binds tighter: no parens under `or`; under `and`, parenthesize
// only the rhs to preserve right-nesting.
return parentOp == "and" && rhs
}
return false
}
func notBodyNeedsParens(b Body) bool {
if len(b) != 1 {
return false
}
e := b[0]
if len(e.With) > 0 {
return true
}
switch e.Terms.(type) {
case *LogicalOr, *LogicalAnd:
// `not` binds tighter than `and`/`or`
return true
}
return false
}
func (w *With) String() string {
buf, _ := w.AppendText(make([]byte, 0, w.StringLength()))
return util.ByteSliceToString(buf)
@@ -1982,27 +1723,6 @@ 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 := withJSON{
Target: w.Target,
Value: w.Value,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
data.Location = w.Location
}
return json.Marshal(data)
}
// Copy returns a deep copy of the AST node x. If x is not an AST node, x is returned unmodified.
func Copy(x any) any {
switch x := x.(type) {
@@ -2136,14 +1856,6 @@ func isGlobalBuiltin(expr *Expr, name Var) bool {
return false
}
// NOTE(tsandall): do not use Term#Equal or Value#Compare to avoid
// allocation here.
ref, ok := terms[0].Value.(Ref)
if !ok || len(ref) != 1 {
return false
}
if head, ok := ref[0].Value.(Var); ok {
return head.Equal(name)
}
return false
return ok && len(ref) == 1 && name.Equal(ref[0].Value)
}

View File

@@ -223,6 +223,10 @@ func (a Args) AppendText(buf []byte) ([]byte, error) {
return append(buf, ')'), nil
}
func (body Body) AppendText(buf []byte) ([]byte, error) {
return AppendDelimeted(buf, body, "; ")
}
func (expr *Expr) AppendText(buf []byte) ([]byte, error) {
if expr.Negated {
buf = append(buf, "not "...)
@@ -335,17 +339,25 @@ func (o *LogicalOr) AppendText(buf []byte) ([]byte, error) {
func appendLogical(buf []byte, op string, lhs, rhs Body, explicitLhs, explicitRhs bool) ([]byte, error) {
var err error
if buf, err = appendLogicalOperand(buf, lhs, explicitLhs); err != nil {
if buf, err = appendLogicalOperand(buf, lhs, explicitLhs, op, false); err != nil {
return nil, err
}
buf = append(buf, ' ')
buf = append(buf, op...)
buf = append(buf, ' ')
return appendLogicalOperand(buf, rhs, explicitRhs)
return appendLogicalOperand(buf, rhs, explicitRhs, op, true)
}
func appendLogicalOperand(buf []byte, b Body, explicit bool) ([]byte, error) {
func appendLogicalOperand(buf []byte, b Body, explicit bool, parentOp string, rhs bool) ([]byte, error) {
if !explicit && len(b) == 1 {
if logicalOperandNeedsParens(b, parentOp, rhs) {
buf = append(buf, '(')
var err error
if buf, err = b.AppendText(buf); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
return b.AppendText(buf)
}

View File

@@ -0,0 +1,289 @@
//go:build !go1.27
package ast
import (
"encoding/json"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
// 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"`
}
// 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"`
}
// 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"`
}
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
// will have their module pointer set to mod.
func (mod *Module) UnmarshalJSON(bs []byte) error {
// Declare a new type and use a type conversion to avoid recursively calling
// Module#UnmarshalJSON.
type module Module
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
return err
}
// The decoded rules have no module pointer, as it isn't part of the JSON
// representation; without this, an unmarshalled module can't be compiled.
WalkRules(mod, func(rule *Rule) bool {
rule.Module = mod
return false
})
return nil
}
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
data := map[string]any{
"symbols": d.Symbols,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
if d.Location != nil {
data["location"] = d.Location
}
}
return json.Marshal(data)
}
func (q *Every) MarshalJSON() ([]byte, error) {
data := map[string]any{
"key": q.Key,
"value": q.Value,
"domain": q.Domain,
"body": q.Body,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every {
if q.Location != nil {
data["location"] = q.Location
}
}
return json.Marshal(data)
}
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "and",
"lhs": a.Lhs,
"rhs": a.Rhs,
}
if a.ExplicitLhs {
data["explicit_lhs"] = true
}
if a.ExplicitRhs {
data["explicit_rhs"] = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And {
if a.Location != nil {
data["location"] = a.Location
}
}
return json.Marshal(data)
}
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
}
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "or",
"lhs": o.Lhs,
"rhs": o.Rhs,
}
if o.ExplicitLhs {
data["explicit_lhs"] = true
}
if o.ExplicitRhs {
data["explicit_rhs"] = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or {
if o.Location != nil {
data["location"] = o.Location
}
}
return json.Marshal(data)
}
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
}
// UnmarshalJSON parses the byte array and stores the result in expr.
func (expr *Expr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalExpr(expr, v)
}
func (expr *Expr) MarshalJSON() ([]byte, error) {
data := exprJSON{
Index: expr.Index,
Terms: expr.Terms,
}
if len(expr.With) > 0 {
data.With = expr.With
}
if expr.Generated {
data.Generated = true
}
if expr.Negated {
data.Negated = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
data.Location = expr.Location
}
return json.Marshal(data)
}
func (w *With) MarshalJSON() ([]byte, error) {
data := withJSON{
Target: w.Target,
Value: w.Value,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
data.Location = w.Location
}
return json.Marshal(data)
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": pkg.Path,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package {
if pkg.Location != nil {
data["location"] = pkg.Location
}
}
return json.Marshal(data)
}
func (imp *Import) MarshalJSON() ([]byte, error) {
data := map[string]any{
"path": imp.Path,
}
if len(imp.Alias) != 0 {
data["alias"] = imp.Alias
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import {
if imp.Location != nil {
data["location"] = imp.Location
}
}
return json.Marshal(data)
}
func (rule *Rule) MarshalJSON() ([]byte, error) {
data := ruleJSON{
Head: rule.Head,
Body: rule.Body,
}
if rule.Default {
data.Default = true
}
if rule.Else != nil {
data.Else = rule.Else
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
data.Location = rule.Location
}
if len(rule.Annotations) != 0 {
data.Annotations = rule.Annotations
}
return json.Marshal(data)
}
func (head *Head) MarshalJSON() ([]byte, error) {
var loc *Location
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && head.Location != nil {
loc = head.Location
}
// NOTE(sr): we do this to override the rendering of `head.Reference`.
// It's still what'll be used via the default means of encoding/json
// for unmarshaling a json object into a Head struct!
type h Head
return json.Marshal(struct {
h
Ref Ref `json:"ref"`
Location *Location `json:"location,omitempty"`
}{
h: h(*head),
Ref: head.Ref(),
Location: loc,
})
}
// MarshalJSON returns JSON encoded bytes representing body.
func (body Body) MarshalJSON() ([]byte, error) {
// Serialize empty Body to empty array. This handles both the empty case and the
// nil case (whereas by default the result would be null if body was nil.)
if len(body) == 0 {
return []byte(`[]`), nil
}
ret, err := json.Marshal([]*Expr(body))
return ret, err
}

View File

@@ -0,0 +1,524 @@
//go:build go1.27
package ast
import (
"encoding/base64"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"github.com/open-policy-agent/opa/internal/jsonv2"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
var (
_ json.Unmarshaler = &Module{}
// These are exported types, so losing MarshalJSON here would be a breaking
// API change even though callers should go through json.Marshal, not this
// method directly.
_ json.Marshaler = Body{}
_ json.Marshaler = &Expr{}
_ json.Marshaler = &Package{}
_ json.Marshaler = &Import{}
_ json.Marshaler = &Rule{}
_ json.Marshaler = &Head{}
_ json.Marshaler = &With{}
_ json.Marshaler = &SomeDecl{}
_ json.Marshaler = &Every{}
_ json.Marshaler = &LogicalAnd{}
_ json.Marshaler = &LogicalOr{}
)
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
// will have their module pointer set to mod.
func (mod *Module) UnmarshalJSON(bs []byte) error {
// Declare a new type and use a type conversion to avoid recursively calling
// Module#UnmarshalJSON.
type module Module
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
return err
}
// The decoded rules have no module pointer, as it isn't part of the JSON
// representation; without this, an unmarshalled module can't be compiled.
WalkRules(mod, func(rule *Rule) bool {
rule.Module = mod
return false
})
return nil
}
// MarshalJSONTo is here to ensure that we do not fall down to TextAppender,
// which Go 1.27's encoding/json would otherwise use, encoding args as the Rego
// representation of the argument list rather than as a JSON array.
func (a Args) MarshalJSONTo(e *jsontext.Encoder) error {
return jsonv2.WriteMarshalerToArrayOrNull(e, a)
}
// MarshalJSONTo is here to ensure that we do not fall down to TextAppender,
// which Go 1.27's encoding/json would otherwise use, encoding the module as
// Rego source rather than as JSON. Module's own fields are fully described by
// their struct tags, so the encoding is left to them, as it is pre-1.27. The
// field types provide their own MarshalJSONTo where one is needed.
func (m *Module) MarshalJSONTo(e *jsontext.Encoder) error {
// Declare a new type and use a type conversion to avoid recursively calling
// Module#MarshalJSONTo. It's the highest precedence marshaller, so there is
// nothing below it to fall to, and the new type has no methods of its own.
type module Module
return json.MarshalEncode(e, (*module)(m))
}
func (pkg *Package) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package && pkg.Location != nil {
if err := jsonv2.WriteField(e, "location", pkg.Location); err != nil {
return err
}
}
if err := jsonv2.WriteField(e, "path", pkg.Path); err != nil {
return err
}
return e.WriteToken(jsontext.EndObject)
}
func (i *Import) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if err := jsonv2.WriteField(e, "path", i.Path); err != nil {
return err
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import && i.Location != nil {
if err := jsonv2.WriteField(e, "location", i.Location); err != nil {
return err
}
}
if len(i.Alias) > 0 {
e.WriteToken(jsontext.String("alias"))
e.WriteToken(jsontext.String(string(i.Alias)))
}
return e.WriteToken(jsontext.EndObject)
}
func (r *Rule) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if r.Default {
e.WriteToken(jsontext.String("default"))
e.WriteToken(jsontext.True)
}
if r.Else != nil {
if err := jsonv2.WriteField(e, "else", r.Else); err != nil {
return err
}
}
if err := jsonv2.WriteField(e, "head", r.Head); err != nil {
return err
}
if err := jsonv2.WriteField(e, "body", r.Body); err != nil {
return err
}
if len(r.Annotations) > 0 {
if err := jsonv2.WriteFieldArray(e, "annotations", r.Annotations); err != nil {
return err
}
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule && r.Location != nil {
if err := jsonv2.WriteField(e, "location", r.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (h *Head) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if h.Name != "" {
e.WriteToken(jsontext.String("name"))
e.WriteToken(jsontext.String(string(h.Name)))
}
if err := jsonv2.WriteField(e, "ref", h.Ref()); err != nil {
return err
}
if len(h.Args) > 0 {
if err := jsonv2.WriteFieldArray(e, "args", h.Args); err != nil {
return err
}
}
if h.Key != nil {
if err := jsonv2.WriteField(e, "key", h.Key); err != nil {
return err
}
}
if h.Value != nil {
if err := jsonv2.WriteField(e, "value", h.Value); err != nil {
return err
}
}
if h.Assign {
e.WriteToken(jsontext.String("assign"))
e.WriteToken(jsontext.True)
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && h.Location != nil {
if err := jsonv2.WriteField(e, "location", h.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (c Call) MarshalJSONTo(e *jsontext.Encoder) (err error) {
return jsonv2.WriteMarshalerToArrayOrNull(e, c)
}
func (c *Comment) MarshalJSONTo(e *jsontext.Encoder) error {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginObject)
// Comment has no JSON tags, hence the capitalised keys, the base64 encoded
// text, and the location being written even when it's nil.
e.WriteToken(jsontext.String("Text"))
buf := make([]byte, base64.StdEncoding.EncodedLen(len(c.Text)))
base64.StdEncoding.Encode(buf, c.Text)
e.WriteValue(append(append(append(e.AvailableBuffer(), '"'), buf...), '"'))
e.WriteToken(jsontext.String("Location"))
if c.Location != nil {
if err := c.Location.MarshalJSONTo(e); err != nil {
return err
}
} else {
e.WriteToken(jsontext.Null)
}
return e.WriteToken(jsontext.EndObject)
}
func (q *Every) MarshalJSONTo(e *jsontext.Encoder) error {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("key"))
if q.Key == nil {
e.WriteToken(jsontext.Null)
} else {
if err := q.Key.MarshalJSONTo(e); err != nil {
return err
}
}
if err := jsonv2.WriteField(e, "value", q.Value); err != nil {
return err
}
if err := jsonv2.WriteField(e, "domain", q.Domain); err != nil {
return err
}
if err := jsonv2.WriteField(e, "body", q.Body); err != nil {
return err
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every && q.Location != nil {
if err := jsonv2.WriteField(e, "location", q.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (b Body) MarshalJSONTo(e *jsontext.Encoder) error {
return jsonv2.WriteMarshalerToArray(e, b)
}
// MarshalJSON returns JSON encoded bytes representing body.
func (body Body) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(body)
}
func (expr *Expr) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(expr)
}
// UnmarshalJSON parses the byte array and stores the result in expr.
func (expr *Expr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalExpr(expr, v)
}
func (e *Expr) MarshalJSONTo(enc *jsontext.Encoder) error {
enc.WriteToken(jsontext.BeginObject)
enc.WriteToken(jsontext.String("index"))
enc.WriteToken(jsontext.Int(int64(e.Index)))
includeLocation := astJSON.GetOptions().MarshalOptions.IncludeLocation
if e.Location != nil && includeLocation.Expr {
if err := jsonv2.WriteField(enc, "location", e.Location); err != nil {
return err
}
}
if e.Negated {
enc.WriteToken(jsontext.String("negated"))
enc.WriteToken(jsontext.True)
}
if e.Generated {
enc.WriteToken(jsontext.String("generated"))
enc.WriteToken(jsontext.True)
}
enc.WriteToken(jsontext.String("terms"))
var err error
switch t := e.Terms.(type) {
case []*Term:
err = jsonv2.WriteMarshalerToArrayOrNull(enc, t)
case json.MarshalerTo:
err = t.MarshalJSONTo(enc)
default:
return fmt.Errorf("unsupported expr terms type: %T", e.Terms)
}
if err != nil {
return fmt.Errorf("failed to marshal expr terms: %w", err)
}
if len(e.With) > 0 {
if err := jsonv2.WriteFieldArray(enc, "with", e.With); err != nil {
return err
}
}
return enc.WriteToken(jsontext.EndObject)
}
func (a *LogicalAnd) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("type"))
e.WriteToken(jsontext.String("and"))
if err := jsonv2.WriteField(e, "lhs", a.Lhs); err != nil {
return err
}
if err := jsonv2.WriteField(e, "rhs", a.Rhs); err != nil {
return err
}
if a.ExplicitLhs {
e.WriteToken(jsontext.String("explicit_lhs"))
e.WriteToken(jsontext.True)
}
if a.ExplicitRhs {
e.WriteToken(jsontext.String("explicit_rhs"))
e.WriteToken(jsontext.True)
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And && a.Location != nil {
if err := jsonv2.WriteField(e, "location", a.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
}
func (o *LogicalOr) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("type"))
e.WriteToken(jsontext.String("or"))
if err := jsonv2.WriteField(e, "lhs", o.Lhs); err != nil {
return err
}
if err := jsonv2.WriteField(e, "rhs", o.Rhs); err != nil {
return err
}
if o.ExplicitLhs {
e.WriteToken(jsontext.String("explicit_lhs"))
e.WriteToken(jsontext.True)
}
if o.ExplicitRhs {
e.WriteToken(jsontext.String("explicit_rhs"))
e.WriteToken(jsontext.True)
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or && o.Location != nil {
if err := jsonv2.WriteField(e, "location", o.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
}
func (w *With) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if err := jsonv2.WriteField(e, "target", w.Target); err != nil {
return err
}
if err := jsonv2.WriteField(e, "value", w.Value); err != nil {
return err
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With && w.Location != nil {
if err := jsonv2.WriteField(e, "location", w.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (d *SomeDecl) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("symbols"))
if err := jsonv2.WriteMarshalerToArrayOrNull(e, d.Symbols); err != nil {
return err
}
if d.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
if err := jsonv2.WriteField(e, "location", d.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (ac *ArrayComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if err := jsonv2.WriteField(e, "term", ac.Term); err != nil {
return err
}
if err := jsonv2.WriteField(e, "body", ac.Body); err != nil {
return err
}
return e.WriteToken(jsontext.EndObject)
}
func (sc *SetComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if err := jsonv2.WriteField(e, "term", sc.Term); err != nil {
return err
}
if err := jsonv2.WriteField(e, "body", sc.Body); err != nil {
return err
}
return e.WriteToken(jsontext.EndObject)
}
func (oc *ObjectComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
if err := jsonv2.WriteField(e, "key", oc.Key); err != nil {
return err
}
if err := jsonv2.WriteField(e, "value", oc.Value); err != nil {
return err
}
if err := jsonv2.WriteField(e, "body", oc.Body); err != nil {
return err
}
return e.WriteToken(jsontext.EndObject)
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(pkg)
}
func (imp *Import) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(imp)
}
func (rule *Rule) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(rule)
}
func (head *Head) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(head)
}
func (w *With) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(w)
}
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(d)
}
func (q *Every) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(q)
}
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(a)
}
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(o)
}

View File

@@ -365,6 +365,10 @@ func (c *Comment) StringLength() int {
func (not *Not) StringLength() int {
if !not.ExplicitBody && len(not.Body) == 1 {
if notBodyNeedsParens(not.Body) {
// "not (...)"
return 6 + not.Body.StringLength()
}
// "not ..."
return 4 + not.Body.StringLength()
}
@@ -373,19 +377,22 @@ func (not *Not) StringLength() int {
}
func (a *LogicalAnd) StringLength() int {
return logicalOperandStringLength(a.Lhs, a.ExplicitLhs) +
return logicalOperandStringLength(a.Lhs, a.ExplicitLhs, "and", false) +
5 + // " and "
logicalOperandStringLength(a.Rhs, a.ExplicitRhs)
logicalOperandStringLength(a.Rhs, a.ExplicitRhs, "and", true)
}
func (o *LogicalOr) StringLength() int {
return logicalOperandStringLength(o.Lhs, o.ExplicitLhs) +
return logicalOperandStringLength(o.Lhs, o.ExplicitLhs, "or", false) +
4 + // " or "
logicalOperandStringLength(o.Rhs, o.ExplicitRhs)
logicalOperandStringLength(o.Rhs, o.ExplicitRhs, "or", true)
}
func logicalOperandStringLength(b Body, explicit bool) int {
func logicalOperandStringLength(b Body, explicit bool, parentOp string, rhs bool) int {
if !explicit && len(b) == 1 {
if logicalOperandNeedsParens(b, parentOp, rhs) {
return b.StringLength() + 2 // "(" + body + ")"
}
return b.StringLength()
}
return b.StringLength() + 4 // "{ " + body + " }"

View File

@@ -19,7 +19,6 @@ import (
"unicode"
"github.com/cespare/xxhash/v2"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/ast/location"
"github.com/open-policy-agent/opa/v1/util"
)
@@ -380,15 +379,12 @@ func (term *Term) Copy() *Term {
// Equal returns true if this term equals the other term. Equality is
// defined for each kind of term, and does not compare the Location.
func (term *Term) Equal(other *Term) bool {
if term == nil && other != nil {
return false
}
if term != nil && other == nil {
return false
}
if term == other {
return true
}
if term == nil || other == nil {
return false
}
return ValueEqual(term.Value, other.Value)
}
@@ -423,55 +419,10 @@ 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 := termJSON{
Type: ValueName(term.Value),
Value: term.Value,
}
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.IncludeLocation.Term {
d.Location = term.Location
}
return json.Marshal(d)
}
func (term *Term) String() string {
return term.Value.String()
}
// UnmarshalJSON parses the byte array and stores the result in term.
// Specialized unmarshalling is required to handle Value and Location.
func (term *Term) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
val, err := unmarshalValue(v)
if err != nil {
return err
}
term.Value = val
if loc, ok := v["location"].(map[string]any); ok {
term.Location = &Location{}
err := unmarshalLocation(term.Location, loc)
if err != nil {
return err
}
}
return nil
}
// Vars returns a VarSet with variables contained in this term.
func (term *Term) Vars() VarSet {
vis := NewVarVisitor()
@@ -654,62 +605,15 @@ func (n *Not) IsGround() bool {
func (n *Not) String() string {
if !n.ExplicitBody && len(n.Body) == 1 {
if notBodyNeedsParens(n.Body) {
return "not (" + n.Body.String() + ")"
}
return "not " + n.Body.String()
}
return "not {" + n.Body.String() + "}"
}
func (n *Not) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "not",
"body": n.Body,
"explicit_body": n.ExplicitBody,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not {
if n.Location != nil {
data["location"] = n.Location
}
}
return json.Marshal(data)
}
func (n *Not) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalNot(n, v)
}
func unmarshalNot(n *Not, v map[string]any) error {
var eb bool
if x, ok := v["explicit_body"]; ok {
eb, ok = x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"])
}
}
b, ok := v["body"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"])
}
body, err := unmarshalBody(b)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal not body: %w", err)
}
n.ExplicitBody = eb
n.Body = body
return nil
}
// Null represents the null value defined by JSON.
type Null struct{}
@@ -874,7 +778,7 @@ func (num Number) Find(path Ref) (Value, error) {
// Hash returns the hash code for the Value.
func (num Number) Hash() int {
if len(num) < 4 {
if i, err := strconv.Atoi(string(num)); err == nil {
if i, ok := util.Atoi(string(num)); ok {
return i
}
}
@@ -892,11 +796,7 @@ func (num Number) Int() (int, bool) {
// Int64 returns the int64 representation of num if possible.
func (num Number) Int64() (int64, bool) {
i, err := json.Number(num).Int64()
if err != nil {
return 0, false
}
return i, true
return util.Atoi64(string(num))
}
// Float64 returns the float64 representation of num if possible.
@@ -913,11 +813,6 @@ func (Number) IsGround() bool {
return true
}
// MarshalJSON returns JSON encoded bytes representing num.
func (num Number) MarshalJSON() ([]byte, error) {
return json.Marshal(json.Number(num))
}
func (num Number) String() string {
return string(num)
}
@@ -1696,14 +1591,6 @@ func (arr *Array) IsGround() bool {
return arr.ground
}
// MarshalJSON returns JSON encoded bytes representing arr.
func (arr *Array) MarshalJSON() ([]byte, error) {
if len(arr.elems) == 0 {
return []byte(`[]`), nil
}
return json.Marshal(arr.elems)
}
func (arr *Array) String() string {
buf, _ := arr.AppendText(make([]byte, 0, arr.StringLength()))
return util.ByteSliceToString(buf)
@@ -2052,14 +1939,6 @@ func (s *set) Len() int {
return len(s.keys)
}
// MarshalJSON returns JSON encoded bytes representing s.
func (s *set) MarshalJSON() ([]byte, error) {
if s.keys == nil {
return []byte(`[]`), nil
}
return json.Marshal(s.sortedKeys())
}
// Sorted returns an Array that contains the sorted elements of s.
func (s *set) Sorted() *Array {
cpy := make([]*Term, len(s.keys))
@@ -2228,10 +2107,6 @@ func (l *lazyObj) Map(f func(*Term, *Term) (*Term, *Term, error)) (Object, error
return l.force().Map(f)
}
func (l *lazyObj) MarshalJSON() ([]byte, error) {
return l.force().(*object).MarshalJSON()
}
func (l *lazyObj) Merge(other Object) (Object, bool) {
return l.force().Merge(other)
}
@@ -2609,15 +2484,6 @@ func (obj *object) KeysIterator() ObjectKeysIterator {
return newobjectKeysIterator(obj)
}
// MarshalJSON returns JSON encoded bytes representing obj.
func (obj *object) MarshalJSON() ([]byte, error) {
sl := make([][2]*Term, obj.Len())
for i, node := range obj.sortedKeys() {
sl[i] = Item(node.key, node.value)
}
return json.Marshal(sl)
}
// Merge returns a new Object containing the non-overlapping keys of obj and other. If there are
// overlapping keys between obj and other, the values of associated with the keys are merged. Only
// objects can be merged with other objects. If the values cannot be merged, the second turn value
@@ -3191,6 +3057,29 @@ func isControlOrBackslash(r rune) bool {
// on the happy path and treats all errors the same. If better error
// reporting is needed, the error paths will need to be fleshed out.
// UnmarshalJSON parses the byte array and stores the result in term.
// Specialized unmarshalling is required to handle Value and Location.
func (term *Term) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
val, err := unmarshalValue(v)
if err != nil {
return err
}
term.Value = val
if loc, ok := v["location"].(map[string]any); ok {
term.Location = &Location{}
err := unmarshalLocation(term.Location, loc)
if err != nil {
return err
}
}
return nil
}
func unmarshalBody(b []any) (Body, error) {
buf := Body{}
for _, e := range b {
@@ -3395,6 +3284,45 @@ func unmarshalWith(i any) (*With, error) {
return nil, errors.New(`ast: unable to unmarshal with modifier (expected {"target": {...}, "value": {...}})`)
}
func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error {
lhsRaw, ok := v["lhs"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"])
}
l, err := unmarshalBody(lhsRaw)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err)
}
*lhs = l
rhsRaw, ok := v["rhs"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"])
}
r, err := unmarshalBody(rhsRaw)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err)
}
*rhs = r
if x, ok := v["explicit_lhs"]; ok {
b, ok := x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x)
}
*explicitLhs = b
}
if x, ok := v["explicit_rhs"]; ok {
b, ok := x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x)
}
*explicitRhs = b
}
return nil
}
func unmarshalValue(d map[string]any) (Value, error) {
v := d["value"]
switch d["type"] {
@@ -3512,3 +3440,28 @@ func unmarshalValue(d map[string]any) (Value, error) {
unmarshal_error:
return nil, errors.New("ast: unable to unmarshal term")
}
func unmarshalNot(n *Not, v map[string]any) error {
var eb bool
if x, ok := v["explicit_body"]; ok {
eb, ok = x.(bool)
if !ok {
return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"])
}
}
b, ok := v["body"].([]any)
if !ok {
return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"])
}
body, err := unmarshalBody(b)
if err != nil {
return fmt.Errorf("ast: unable to unmarshal not body: %w", err)
}
n.ExplicitBody = eb
n.Body = body
return nil
}

View File

@@ -289,6 +289,14 @@ func appendComprehensionTerm(buf []byte, term *Term) ([]byte, error) {
func (not *Not) AppendText(buf []byte) ([]byte, error) {
if !not.ExplicitBody && len(not.Body) == 1 {
if notBodyNeedsParens(not.Body) {
buf = append(buf, "not ("...)
var err error
if buf, err = not.Body.AppendText(buf); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
buf = append(buf, "not "...)
return not.Body.AppendText(buf)
}

View File

@@ -0,0 +1,95 @@
// 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.
//go:build !go1.27
package ast
import (
"encoding/json"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
// 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 := termJSON{
Type: ValueName(term.Value),
Value: term.Value,
}
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.IncludeLocation.Term {
d.Location = term.Location
}
return json.Marshal(d)
}
// MarshalJSON returns JSON encoded bytes representing arr.
func (arr *Array) MarshalJSON() ([]byte, error) {
if len(arr.elems) == 0 {
return []byte(`[]`), nil
}
return json.Marshal(arr.elems)
}
// MarshalJSON returns JSON encoded bytes representing num.
func (num Number) MarshalJSON() ([]byte, error) {
return json.Marshal(json.Number(num))
}
// MarshalJSON returns JSON encoded bytes representing obj.
func (obj *object) MarshalJSON() ([]byte, error) {
sl := make([][2]*Term, obj.Len())
for i, node := range obj.sortedKeys() {
sl[i] = Item(node.key, node.value)
}
return json.Marshal(sl)
}
// MarshalJSON returns JSON encoded bytes representing s.
func (s *set) MarshalJSON() ([]byte, error) {
if s.keys == nil {
return []byte(`[]`), nil
}
return json.Marshal(s.sortedKeys())
}
func (l *lazyObj) MarshalJSON() ([]byte, error) {
return l.force().(*object).MarshalJSON()
}
func (n *Not) MarshalJSON() ([]byte, error) {
data := map[string]any{
"type": "not",
"body": n.Body,
"explicit_body": n.ExplicitBody,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not {
if n.Location != nil {
data["location"] = n.Location
}
}
return json.Marshal(data)
}
func (n *Not) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalNot(n, v)
}

View File

@@ -0,0 +1,251 @@
//go:build go1.27
package ast
import (
"encoding"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"github.com/open-policy-agent/opa/internal/jsonv2"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
var (
_ json.MarshalerTo = &Term{}
_ json.Unmarshaler = &LogicalOr{}
_ json.MarshalerTo = &LogicalOr{}
_ json.MarshalerTo = &Not{}
_ json.MarshalerTo = &Array{}
_ json.MarshalerTo = &set{}
_ json.MarshalerTo = &object{}
_ json.MarshalerTo = &TemplateString{}
_ json.MarshalerTo = &Ref{}
_ json.MarshalerTo = &lazyObj{}
_ json.MarshalerTo = Args{}
_ json.MarshalerTo = Boolean(false)
_ json.MarshalerTo = Null{}
_ json.MarshalerTo = Number("")
_ json.MarshalerTo = String("")
_ json.MarshalerTo = Var("")
_ json.Unmarshaler = &Not{}
// These are exported types, so losing MarshalJSON here would be a breaking
// API change even though callers should go through json.Marshal, not this
// method directly.
_ json.Marshaler = Number("")
_ json.Marshaler = &Term{}
_ json.Marshaler = &Not{}
_ json.Marshaler = &lazyObj{}
_ json.Marshaler = &object{}
_ json.Marshaler = &Array{}
_ json.Marshaler = &set{}
)
// These are here to ensure that we do not fall down to TextAppender, which
// Go 1.27's encoding/json would otherwise use, encoding these as JSON strings.
func (b Boolean) MarshalJSONTo(e *jsontext.Encoder) error {
return e.WriteToken(jsontext.Bool(bool(b)))
}
func (Null) MarshalJSONTo(e *jsontext.Encoder) error {
// Encoded as an empty object rather than null, as that's the representation
// callers have come to expect. See also [marshalValueTo].
return e.WriteValue([]byte("{}"))
}
func (v Var) MarshalJSONTo(e *jsontext.Encoder) error {
// Must produce the var name as a JSON string, wildcard vars included: that's
// what encoding/json v1 does for a type whose underlying kind is string.
return e.WriteToken(jsontext.String(string(v)))
}
func (num Number) MarshalJSONTo(e *jsontext.Encoder) error {
if num == "" {
// Matches encoding/json v1, which encodes an empty json.Number as 0.
return e.WriteToken(jsontext.Int(0))
}
return e.WriteValue(jsontext.Value(num))
}
// MarshalJSON returns JSON encoded bytes representing num.
func (num Number) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(num)
}
func (str String) MarshalJSONTo(e *jsontext.Encoder) error {
return e.WriteToken(jsontext.String(string(str)))
}
func (t *Term) MarshalJSONTo(e *jsontext.Encoder) (err error) {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginObject)
includeLocation := astJSON.GetOptions().MarshalOptions.IncludeLocation
if t.Location != nil && includeLocation.Term {
if err := jsonv2.WriteField(e, "location", t.Location); err != nil {
return err
}
}
e.WriteToken(jsontext.String("type"))
e.WriteToken(jsontext.String(ValueName(t.Value)))
e.WriteToken(jsontext.String("value"))
if err = marshalValueTo(e, t.Value); err != nil {
return fmt.Errorf("failed to marshal term of %s: %w", ValueName(t.Value), err)
}
return e.WriteToken(jsontext.EndObject)
}
// MarshalJSON returns the JSON encoding of the term.
func (term *Term) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(term)
}
func (r Ref) MarshalJSONTo(e *jsontext.Encoder) (err error) {
return jsonv2.WriteMarshalerToArrayOrNull(e, r)
}
func (t *TemplateString) MarshalJSONTo(e *jsontext.Encoder) (err error) {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("parts"))
if t.Parts == nil {
// Parts has no omitempty tag, so it's always written. Matches
// encoding/json v1, which encodes a nil slice as null rather than as an
// empty array.
e.WriteToken(jsontext.Null)
} else {
e.WriteToken(jsontext.BeginArray)
for _, p := range t.Parts {
switch v := p.(type) {
case *Expr:
if err := v.MarshalJSONTo(e); err != nil {
return err
}
case *Term:
if err := v.MarshalJSONTo(e); err != nil {
return err
}
}
}
e.WriteToken(jsontext.EndArray)
}
e.WriteToken(jsontext.String("multi_line"))
e.WriteToken(jsontext.Bool(t.MultiLine))
return e.WriteToken(jsontext.EndObject)
}
func (n *Not) MarshalJSONTo(e *jsontext.Encoder) error {
e.WriteToken(jsontext.BeginObject)
e.WriteToken(jsontext.String("type"))
e.WriteToken(jsontext.String("not"))
if err := jsonv2.WriteField(e, "body", n.Body); err != nil {
return err
}
e.WriteToken(jsontext.String("explicit_body"))
e.WriteToken(jsontext.Bool(n.ExplicitBody))
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not && n.Location != nil {
if err := jsonv2.WriteField(e, "location", n.Location); err != nil {
return err
}
}
return e.WriteToken(jsontext.EndObject)
}
func (n *Not) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(n)
}
func (n *Not) UnmarshalJSON(bs []byte) error {
v := map[string]any{}
if err := util.UnmarshalJSON(bs, &v); err != nil {
return err
}
return unmarshalNot(n, v)
}
func (obj *object) MarshalJSONTo(e *jsontext.Encoder) error {
// Token write errors are unchecked: an unbalanced value fails at the closing
// token. A marshaller can fail having written a balanced value, so is checked.
e.WriteToken(jsontext.BeginArray)
for _, node := range obj.sortedKeys() {
e.WriteToken(jsontext.BeginArray)
if err := node.key.MarshalJSONTo(e); err != nil {
return err
}
if err := node.value.MarshalJSONTo(e); err != nil {
return err
}
e.WriteToken(jsontext.EndArray)
}
return e.WriteToken(jsontext.EndArray)
}
func (l *lazyObj) MarshalJSONTo(e *jsontext.Encoder) error {
return l.force().(*object).MarshalJSONTo(e)
}
func (l *lazyObj) MarshalJSON() ([]byte, error) {
return l.force().(*object).MarshalJSON()
}
// MarshalJSON returns JSON encoded bytes representing obj.
func (obj *object) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(obj)
}
func (a *Array) MarshalJSONTo(e *jsontext.Encoder) error {
return jsonv2.WriteMarshalerToArray(e, a.elems)
}
// MarshalJSON returns JSON encoded bytes representing arr.
func (arr *Array) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(arr)
}
func (s *set) MarshalJSONTo(e *jsontext.Encoder) error {
return jsonv2.WriteMarshalerToArray(e, s.sortedKeys())
}
// MarshalJSON returns JSON encoded bytes representing s.
func (s *set) MarshalJSON() ([]byte, error) {
return jsonv2.MarshalMarshalerTo(s)
}
func marshalValueTo(e *jsontext.Encoder, val Value) (err error) {
switch v := val.(type) {
case json.MarshalerTo:
err = v.MarshalJSONTo(e)
case encoding.TextAppender:
var text []byte
if text, err = v.AppendText(nil); err != nil {
return err
}
if text, err = jsontext.AppendQuote(e.AvailableBuffer(), text); err != nil {
return err
}
err = e.WriteValue(text)
default:
err = json.MarshalEncode(e, v)
}
return err
}

View File

@@ -845,6 +845,11 @@
"Minor": 36,
"Patch": 0
},
"strings.split_n": {
"Major": 1,
"Minor": 19,
"Patch": 0
},
"substring": {
"Major": 0,
"Minor": 17,

View File

@@ -23,11 +23,13 @@ import (
"github.com/gobwas/glob"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
"github.com/open-policy-agent/opa/internal/file/archive"
"github.com/open-policy-agent/opa/internal/merge"
"github.com/open-policy-agent/opa/v1/ast"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
pb "github.com/open-policy-agent/opa/v1/bundle/v1pb"
"github.com/open-policy-agent/opa/v1/format"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/storage"
@@ -39,7 +41,9 @@ const (
RegoExt = ".rego"
WasmFile = "policy.wasm"
PlanFile = "plan.json"
PlanProtoFile = "plan.pb"
ManifestExt = ".manifest"
ManifestProtoExt = ".manifest.pb"
SignaturesFile = "signatures.json"
patchFile = "patch.json"
dataFile = "data.json"
@@ -70,6 +74,13 @@ type Bundle struct {
lazyLoadingMode bool
sizeLimitBytes int64
manifestProto bool
}
// SetManifestProto configures the bundle to serialize its manifest as
// protobuf at /.manifest.pb instead of JSON at /.manifest.
func (b *Bundle) SetManifestProto(yes bool) {
b.manifestProto = yes
}
// Raw contains raw bytes representing the bundle's content
@@ -213,7 +224,7 @@ func (m Manifest) Equal(other Manifest) bool {
// If both are nil, or both are empty, we consider them equal.
if !(len(m.FileRegoVersions) == 0 && len(other.FileRegoVersions) == 0) &&
!reflect.DeepEqual(m.FileRegoVersions, other.FileRegoVersions) {
!maps.Equal(m.FileRegoVersions, other.FileRegoVersions) {
return false
}
@@ -641,6 +652,7 @@ func (r *Reader) Read() (Bundle, error) {
}
var modules []ModuleFile
var manifestPath string
for _, f := range descriptors {
buf, err := readFile(f, r.sizeLimitBytes)
if err != nil {
@@ -695,7 +707,7 @@ func (r *Reader) Read() (Bundle, error) {
Path: r.fullPath(path),
Raw: buf.Bytes(),
})
} else if filepath.Base(path) == PlanFile {
} else if filepath.Base(path) == PlanFile || filepath.Base(path) == PlanProtoFile {
bundle.PlanModules = append(bundle.PlanModules, PlanModuleFile{
URL: f.URL(),
Path: r.fullPath(path),
@@ -741,7 +753,26 @@ func (r *Reader) Read() (Bundle, error) {
return empty, err
}
} else if strings.HasSuffix(path, ManifestProtoExt) {
if manifestPath != "" {
return empty, fmt.Errorf("bundle contains multiple manifest files: %q and %q", manifestPath, path)
}
manifestPath = path
pbManifest := &pb.Manifest{}
if err := proto.Unmarshal(buf.Bytes(), pbManifest); err != nil {
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
}
m, err := ManifestFromProto(pbManifest)
if err != nil {
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
}
bundle.Manifest = *m
bundle.manifestProto = true
} else if strings.HasSuffix(path, ManifestExt) {
if manifestPath != "" {
return empty, fmt.Errorf("bundle contains multiple manifest files: %q and %q", manifestPath, path)
}
manifestPath = path
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
}
@@ -935,6 +966,9 @@ func (w *Writer) DisableFormat(yes bool) *Writer {
// Write writes the bundle to the writer's output stream.
func (w *Writer) Write(bundle Bundle) error {
if err := validateBundleFormat(&bundle); err != nil {
return err
}
tw := archive.NewTarGzWriter(w.w)
if bundle.Type() == SnapshotBundleType {
@@ -971,8 +1005,18 @@ func (w *Writer) Write(bundle Bundle) error {
}
if !bundle.Manifest.Empty() {
if err := tw.WriteJSONFile("/.manifest", bundle.Manifest); err != nil {
return err
if bundle.manifestProto {
bs, err := marshalManifestProto(&bundle.Manifest)
if err != nil {
return err
}
if err := tw.WriteFile(util.WithPrefix(ManifestProtoExt, "/"), bs); err != nil {
return err
}
} else {
if err := tw.WriteJSONFile("/.manifest", bundle.Manifest); err != nil {
return err
}
}
}
@@ -1060,32 +1104,74 @@ func hashBundleFiles(hash SignatureHasher, b *Bundle) ([]FileInfo, error) {
files = append(files, NewFile(strings.TrimPrefix(planmodule.Path, "/"), hex.EncodeToString(bs), defaultHashingAlg))
}
// If the manifest is essentially empty, don't add it to the signatures since it
// won't be written to the bundle. Otherwise:
// parse the manifest into a JSON structure;
// then recursively order the fields of all objects alphabetically and then apply
// the hash function to result to compute the hash.
// Skip empty manifest — Writer.Write skips it too, so no entry to hash.
// Proto manifest is hashed as raw deterministic-marshal bytes (matches
// what VerifyBundleFile sees, since IsStructuredDoc is false for /.manifest.pb).
if !b.Manifest.Empty() {
mbs, err := json.Marshal(b.Manifest)
if err != nil {
return files, err
}
if b.manifestProto {
pbBytes, err := marshalManifestProto(&b.Manifest)
if err != nil {
return files, err
}
if bs, err = hash.HashFile(pbBytes); err != nil {
return files, err
}
files = append(files, NewFile(strings.TrimPrefix(ManifestProtoExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
} else {
mbs, err := json.Marshal(b.Manifest)
if err != nil {
return files, err
}
var result map[string]any
if err := util.Unmarshal(mbs, &result); err != nil {
return files, err
}
var result map[string]any
if err := util.Unmarshal(mbs, &result); err != nil {
return files, err
}
if bs, err = hash.HashFile(result); err != nil {
return files, err
}
if bs, err = hash.HashFile(result); err != nil {
return files, err
}
files = append(files, NewFile(strings.TrimPrefix(ManifestExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
files = append(files, NewFile(strings.TrimPrefix(ManifestExt, "/"), hex.EncodeToString(bs), defaultHashingAlg))
}
}
return files, err
}
// marshalManifestProto returns the deterministic protobuf wire form so
// signer and writer produce byte-identical output (sign/verify on
// /.manifest.pb depends on it).
func marshalManifestProto(m *Manifest) ([]byte, error) {
pbManifest, err := ManifestToProto(m)
if err != nil {
return nil, err
}
return proto.MarshalOptions{Deterministic: true}.Marshal(pbManifest)
}
// validateBundleFormat rejects bundles whose plan format disagrees with
// the manifest format (e.g. /plan.pb + /.manifest).
func validateBundleFormat(b *Bundle) error {
if b.Manifest.Empty() {
return nil
}
for _, pm := range b.PlanModules {
base := filepath.Base(pm.Path)
switch base {
case PlanFile:
if b.manifestProto {
return fmt.Errorf("bundle has proto manifest but JSON plan %q; SetManifestProto must agree with plan format", pm.Path)
}
case PlanProtoFile:
if !b.manifestProto {
return fmt.Errorf("bundle has JSON manifest but proto plan %q; SetManifestProto(true) required", pm.Path)
}
}
}
return nil
}
// FormatModules formats Rego modules
// Modules will be formatted to comply with [ast.DefaultRegoVersion], but Rego compatibility of individual parsed modules will be respected (e.g. if 'rego.v1' is imported).
func (b *Bundle) FormatModules(useModulePath bool) error {
@@ -1473,11 +1559,34 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat
var roots []string
var result Bundle
var planFile string
var manifestProto bool
var manifestProtoSet bool
for _, b := range bundles {
if b.Manifest.Roots == nil {
return nil, errors.New("bundle manifest not initialized")
}
for _, pm := range b.PlanModules {
base := filepath.Base(pm.Path)
if base != PlanFile && base != PlanProtoFile {
continue
}
if planFile == "" {
planFile = base
} else if planFile != base {
return nil, fmt.Errorf("cannot merge bundles with mixed plan formats (%s and %s)", planFile, base)
}
}
if !manifestProtoSet {
manifestProto = b.manifestProto
manifestProtoSet = true
} else if manifestProto != b.manifestProto {
return nil, errors.New("cannot merge bundles with mixed manifest formats")
}
roots = append(roots, *b.Manifest.Roots...)
result.Modules = append(result.Modules, b.Modules...)
@@ -1508,6 +1617,8 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat
}
}
result.manifestProto = manifestProto
// We respect the bundle rego-version, defaulting to the provided rego version if not set.
result.SetRegoVersion(result.RegoVersion(regoVersion))
@@ -1666,7 +1777,9 @@ func modulePathWithPrefix(bundleName string, modulePath string) string {
return path.Join(bundleName, modulePath)
}
// IsStructuredDoc checks if the file name equals a structured file extension ex. ".json"
// IsStructuredDoc checks if the file name equals a structured file extension ex. ".json".
// Note: ManifestProtoExt (".manifest.pb") is intentionally absent — proto manifests are
// hashed as raw wire bytes on both the sign and verify paths.
func IsStructuredDoc(name string) bool {
base := filepath.Base(name)
return base == dataFile || base == yamlDataFile || base == SignaturesFile || base == ManifestExt

View File

@@ -200,7 +200,10 @@ func formatPath(fileName string, root string, pathFormat PathFormat) string {
case Chrooted:
// Trim off the root directory and return path as if chrooted
result := strings.TrimPrefix(fileName, filepath.FromSlash(root))
if root == "." && filepath.Base(fileName) == ManifestExt {
// TrimPrefix at root="." strips the leading dot from dotfile manifests
// (".manifest" → "manifest"), which then misses the Reader's HasSuffix
// check. Restore the original name for both manifest forms.
if root == "." && (filepath.Base(fileName) == ManifestExt || filepath.Base(fileName) == ManifestProtoExt) {
result = fileName
}
if !strings.HasPrefix(result, string(filepath.Separator)) {

View File

@@ -16,7 +16,7 @@ message Manifest {
// Bundle revision string.
string revision = 1;
// Root paths the bundle owns.
// Root paths the bundle owns. See `roots_set` for nil-vs-empty.
repeated string roots = 2;
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
@@ -31,6 +31,10 @@ message Manifest {
// Free-form metadata object. Modeled as `Struct` because the Go field
// is `map[string]any`.
google.protobuf.Struct metadata = 6;
// True if `bundle.Manifest.Roots` was non-nil. `repeated string` can't
// distinguish nil (default to [""]) from explicit-empty (owns no paths).
bool roots_set = 7;
}
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.

View File

@@ -0,0 +1,434 @@
// 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 bundle
import (
"encoding/json"
"fmt"
"net/url"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/ast/location"
pb "github.com/open-policy-agent/opa/v1/bundle/v1pb"
)
// ManifestToProto converts a bundle Manifest to its protobuf wire-form,
// defined in v1/bundle/manifest.proto. The compiled-only fileRegoVersions
// cache is intentionally not modeled. Roots presence (nil vs explicit-empty)
// is preserved via the `roots_set` wire field.
func ManifestToProto(m *Manifest) (*pb.Manifest, error) {
if m == nil {
return nil, nil
}
out := &pb.Manifest{
Revision: proto.String(m.Revision),
}
if m.Roots != nil {
out.Roots = append([]string(nil), (*m.Roots)...)
out.RootsSet = proto.Bool(true)
}
if len(m.WasmResolvers) > 0 {
out.Wasm = make([]*pb.WasmResolver, len(m.WasmResolvers))
for i := range m.WasmResolvers {
wr, err := wasmResolverToProto(&m.WasmResolvers[i])
if err != nil {
return nil, fmt.Errorf("manifest wasm[%d]: %w", i, err)
}
out.Wasm[i] = wr
}
}
if m.RegoVersion != nil {
out.RegoVersion = proto.Int32(int32(*m.RegoVersion))
}
if len(m.FileRegoVersions) > 0 {
out.FileRegoVersions = make(map[string]int32, len(m.FileRegoVersions))
for k, v := range m.FileRegoVersions {
out.FileRegoVersions[k] = int32(v)
}
}
if len(m.Metadata) > 0 {
s, err := jsonNormalizeStruct(m.Metadata)
if err != nil {
return nil, fmt.Errorf("manifest metadata: %w", err)
}
out.Metadata = s
}
return out, nil
}
func wasmResolverToProto(w *WasmResolver) (*pb.WasmResolver, error) {
if w == nil {
return nil, nil
}
out := &pb.WasmResolver{
Entrypoint: proto.String(w.Entrypoint),
Module: proto.String(w.Module),
}
if len(w.Annotations) > 0 {
out.Annotations = make([]*pb.Annotations, len(w.Annotations))
for i, a := range w.Annotations {
ap, err := annotationsToProto(a)
if err != nil {
return nil, fmt.Errorf("annotations[%d]: %w", i, err)
}
out.Annotations[i] = ap
}
}
return out, nil
}
func annotationsToProto(a *ast.Annotations) (*pb.Annotations, error) {
if a == nil {
return nil, nil
}
out := &pb.Annotations{
Scope: proto.String(a.Scope),
Title: proto.String(a.Title),
Entrypoint: proto.Bool(a.Entrypoint),
Description: proto.String(a.Description),
Organizations: append([]string(nil), a.Organizations...),
}
if len(a.RelatedResources) > 0 {
out.RelatedResources = make([]*pb.RelatedResourceAnnotation, len(a.RelatedResources))
for i, r := range a.RelatedResources {
out.RelatedResources[i] = relatedResourceToProto(r)
}
}
if len(a.Authors) > 0 {
out.Authors = make([]*pb.AuthorAnnotation, len(a.Authors))
for i, au := range a.Authors {
out.Authors[i] = authorToProto(au)
}
}
if len(a.Schemas) > 0 {
out.Schemas = make([]*pb.SchemaAnnotation, len(a.Schemas))
for i, s := range a.Schemas {
sa, err := schemaToProto(s)
if err != nil {
return nil, fmt.Errorf("schemas[%d]: %w", i, err)
}
out.Schemas[i] = sa
}
}
if a.Compile != nil {
out.Compile = compileToProto(a.Compile)
}
if len(a.Custom) > 0 {
s, err := jsonNormalizeStruct(a.Custom)
if err != nil {
return nil, fmt.Errorf("custom: %w", err)
}
out.Custom = s
}
if len(a.Labels) > 0 {
s, err := jsonNormalizeStruct(a.Labels)
if err != nil {
return nil, fmt.Errorf("labels: %w", err)
}
out.Labels = s
}
if a.Location != nil {
out.Location = locationToProto(a.Location)
}
return out, nil
}
func relatedResourceToProto(r *ast.RelatedResourceAnnotation) *pb.RelatedResourceAnnotation {
if r == nil {
return nil
}
return &pb.RelatedResourceAnnotation{
Ref: proto.String(r.Ref.String()),
Description: proto.String(r.Description),
}
}
func authorToProto(a *ast.AuthorAnnotation) *pb.AuthorAnnotation {
if a == nil {
return nil
}
return &pb.AuthorAnnotation{
Name: proto.String(a.Name),
Email: proto.String(a.Email),
}
}
func schemaToProto(s *ast.SchemaAnnotation) (*pb.SchemaAnnotation, error) {
if s == nil {
return nil, nil
}
out := &pb.SchemaAnnotation{
Path: proto.String(s.Path.String()),
Schema: proto.String(s.Schema.String()),
}
if s.Definition != nil {
v, err := jsonNormalizeValue(*s.Definition)
if err != nil {
return nil, fmt.Errorf("definition: %w", err)
}
out.Definition = v
}
return out, nil
}
func compileToProto(c *ast.CompileAnnotation) *pb.CompileAnnotation {
if c == nil {
return nil
}
out := &pb.CompileAnnotation{
MaskRule: proto.String(c.MaskRule.String()),
}
if len(c.Unknowns) > 0 {
out.Unknowns = make([]string, len(c.Unknowns))
for i, u := range c.Unknowns {
out.Unknowns[i] = u.String()
}
}
return out
}
func locationToProto(l *location.Location) *pb.Location {
if l == nil {
return nil
}
return &pb.Location{
File: proto.String(l.File),
Row: proto.Int32(int32(l.Row)),
Col: proto.Int32(int32(l.Col)),
}
}
// ManifestFromProto is the inverse of ManifestToProto.
func ManifestFromProto(m *pb.Manifest) (*Manifest, error) {
if m == nil {
return nil, nil
}
out := &Manifest{
Revision: m.GetRevision(),
}
if m.GetRootsSet() {
roots := make([]string, len(m.Roots))
copy(roots, m.Roots)
out.Roots = &roots
}
if len(m.Wasm) > 0 {
out.WasmResolvers = make([]WasmResolver, len(m.Wasm))
for i, wr := range m.Wasm {
converted, err := wasmResolverFromProto(wr)
if err != nil {
return nil, fmt.Errorf("manifest wasm[%d]: %w", i, err)
}
out.WasmResolvers[i] = converted
}
}
if m.RegoVersion != nil {
v := int(*m.RegoVersion)
out.RegoVersion = &v
}
if len(m.FileRegoVersions) > 0 {
out.FileRegoVersions = make(map[string]int, len(m.FileRegoVersions))
for k, v := range m.FileRegoVersions {
out.FileRegoVersions[k] = int(v)
}
}
if m.Metadata != nil {
out.Metadata = m.Metadata.AsMap()
}
return out, nil
}
func wasmResolverFromProto(w *pb.WasmResolver) (WasmResolver, error) {
out := WasmResolver{
Entrypoint: w.GetEntrypoint(),
Module: w.GetModule(),
}
if len(w.Annotations) > 0 {
out.Annotations = make([]*ast.Annotations, len(w.Annotations))
for i, a := range w.Annotations {
converted, err := annotationsFromProto(a)
if err != nil {
return WasmResolver{}, fmt.Errorf("annotations[%d]: %w", i, err)
}
out.Annotations[i] = converted
}
}
return out, nil
}
func annotationsFromProto(a *pb.Annotations) (*ast.Annotations, error) {
if a == nil {
return nil, nil
}
out := &ast.Annotations{
Scope: a.GetScope(),
Title: a.GetTitle(),
Entrypoint: a.GetEntrypoint(),
Description: a.GetDescription(),
}
if len(a.Organizations) > 0 {
out.Organizations = append([]string(nil), a.Organizations...)
}
if len(a.RelatedResources) > 0 {
out.RelatedResources = make([]*ast.RelatedResourceAnnotation, len(a.RelatedResources))
for i, r := range a.RelatedResources {
converted, err := relatedResourceFromProto(r)
if err != nil {
return nil, fmt.Errorf("related_resources[%d]: %w", i, err)
}
out.RelatedResources[i] = converted
}
}
if len(a.Authors) > 0 {
out.Authors = make([]*ast.AuthorAnnotation, len(a.Authors))
for i, au := range a.Authors {
out.Authors[i] = authorFromProto(au)
}
}
if len(a.Schemas) > 0 {
out.Schemas = make([]*ast.SchemaAnnotation, len(a.Schemas))
for i, s := range a.Schemas {
converted, err := schemaFromProto(s)
if err != nil {
return nil, fmt.Errorf("schemas[%d]: %w", i, err)
}
out.Schemas[i] = converted
}
}
if a.Compile != nil {
converted, err := compileFromProto(a.Compile)
if err != nil {
return nil, fmt.Errorf("compile: %w", err)
}
out.Compile = converted
}
if a.Custom != nil {
out.Custom = a.Custom.AsMap()
}
if a.Labels != nil {
out.Labels = a.Labels.AsMap()
}
if a.Location != nil {
out.Location = locationFromProto(a.Location)
}
return out, nil
}
func relatedResourceFromProto(r *pb.RelatedResourceAnnotation) (*ast.RelatedResourceAnnotation, error) {
if r == nil {
return nil, nil
}
out := &ast.RelatedResourceAnnotation{
Description: r.GetDescription(),
}
if ref := r.GetRef(); ref != "" {
u, err := url.Parse(ref)
if err != nil {
return nil, fmt.Errorf("ref %q: %w", ref, err)
}
out.Ref = *u
}
return out, nil
}
func authorFromProto(a *pb.AuthorAnnotation) *ast.AuthorAnnotation {
if a == nil {
return nil
}
return &ast.AuthorAnnotation{
Name: a.GetName(),
Email: a.GetEmail(),
}
}
func schemaFromProto(s *pb.SchemaAnnotation) (*ast.SchemaAnnotation, error) {
if s == nil {
return nil, nil
}
out := &ast.SchemaAnnotation{}
if p := s.GetPath(); p != "" {
ref, err := ast.ParseRef(p)
if err != nil {
return nil, fmt.Errorf("path %q: %w", p, err)
}
out.Path = ref
}
if sc := s.GetSchema(); sc != "" {
ref, err := ast.ParseSchemaRef(sc)
if err != nil {
return nil, fmt.Errorf("schema %q: %w", sc, err)
}
out.Schema = ref
}
if s.Definition != nil {
def := s.Definition.AsInterface()
out.Definition = &def
}
return out, nil
}
func compileFromProto(c *pb.CompileAnnotation) (*ast.CompileAnnotation, error) {
if c == nil {
return nil, nil
}
out := &ast.CompileAnnotation{}
if mr := c.GetMaskRule(); mr != "" {
ref, err := ast.ParseRef(mr)
if err != nil {
return nil, fmt.Errorf("mask_rule %q: %w", mr, err)
}
out.MaskRule = ref
}
if len(c.Unknowns) > 0 {
out.Unknowns = make([]ast.Ref, len(c.Unknowns))
for i, u := range c.Unknowns {
ref, err := ast.ParseRef(u)
if err != nil {
return nil, fmt.Errorf("unknowns[%d] %q: %w", i, u, err)
}
out.Unknowns[i] = ref
}
}
return out, nil
}
func locationFromProto(l *pb.Location) *location.Location {
if l == nil {
return nil
}
return &location.Location{
File: l.GetFile(),
Row: int(l.GetRow()),
Col: int(l.GetCol()),
}
}
// jsonNormalizeStruct routes a map through JSON before structpb.NewStruct
// so the proto path accepts the same value types the JSON path does.
func jsonNormalizeStruct(m map[string]any) (*structpb.Struct, error) {
bs, err := json.Marshal(m)
if err != nil {
return nil, err
}
var normalized map[string]any
if err := json.Unmarshal(bs, &normalized); err != nil {
return nil, err
}
return structpb.NewStruct(normalized)
}
func jsonNormalizeValue(v any) (*structpb.Value, error) {
bs, err := json.Marshal(v)
if err != nil {
return nil, err
}
var normalized any
if err := json.Unmarshal(bs, &normalized); err != nil {
return nil, err
}
return structpb.NewValue(normalized)
}

View File

@@ -0,0 +1,751 @@
// 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.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.35.1
// source: v1/bundle/manifest.proto
package v1pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
structpb "google.golang.org/protobuf/types/known/structpb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Manifest mirrors `bundle.Manifest` in v1/bundle/bundle.go.
type Manifest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Bundle revision string.
Revision *string `protobuf:"bytes,1,opt,name=revision" json:"revision,omitempty"`
// Root paths the bundle owns. See `roots_set` for nil-vs-empty.
Roots []string `protobuf:"bytes,2,rep,name=roots" json:"roots,omitempty"`
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
Wasm []*WasmResolver `protobuf:"bytes,3,rep,name=wasm" json:"wasm,omitempty"`
// Global Rego version for the bundle. Currently 0 (RegoV0) or 1 (RegoV1).
RegoVersion *int32 `protobuf:"varint,4,opt,name=rego_version,json=regoVersion" json:"rego_version,omitempty"`
// Per-file Rego version overrides keyed by file path.
FileRegoVersions map[string]int32 `protobuf:"bytes,5,rep,name=file_rego_versions,json=fileRegoVersions" json:"file_rego_versions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
// Free-form metadata object. Modeled as `Struct` because the Go field
// is `map[string]any`.
Metadata *structpb.Struct `protobuf:"bytes,6,opt,name=metadata" json:"metadata,omitempty"`
// True iff `bundle.Manifest.Roots` was non-nil. `repeated string` can't
// distinguish nil (default to [""]) from explicit-empty (owns no paths).
RootsSet *bool `protobuf:"varint,7,opt,name=roots_set,json=rootsSet" json:"roots_set,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Manifest) Reset() {
*x = Manifest{}
mi := &file_v1_bundle_manifest_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Manifest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Manifest) ProtoMessage() {}
func (x *Manifest) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Manifest.ProtoReflect.Descriptor instead.
func (*Manifest) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{0}
}
func (x *Manifest) GetRevision() string {
if x != nil && x.Revision != nil {
return *x.Revision
}
return ""
}
func (x *Manifest) GetRoots() []string {
if x != nil {
return x.Roots
}
return nil
}
func (x *Manifest) GetWasm() []*WasmResolver {
if x != nil {
return x.Wasm
}
return nil
}
func (x *Manifest) GetRegoVersion() int32 {
if x != nil && x.RegoVersion != nil {
return *x.RegoVersion
}
return 0
}
func (x *Manifest) GetFileRegoVersions() map[string]int32 {
if x != nil {
return x.FileRegoVersions
}
return nil
}
func (x *Manifest) GetMetadata() *structpb.Struct {
if x != nil {
return x.Metadata
}
return nil
}
func (x *Manifest) GetRootsSet() bool {
if x != nil && x.RootsSet != nil {
return *x.RootsSet
}
return false
}
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.
type WasmResolver struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Entrypoint policy ref this resolver targets.
Entrypoint *string `protobuf:"bytes,1,opt,name=entrypoint" json:"entrypoint,omitempty"`
// Path to the wasm module within the bundle.
Module *string `protobuf:"bytes,2,opt,name=module" json:"module,omitempty"`
// Rego annotations attached to the entrypoint.
Annotations []*Annotations `protobuf:"bytes,3,rep,name=annotations" json:"annotations,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WasmResolver) Reset() {
*x = WasmResolver{}
mi := &file_v1_bundle_manifest_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *WasmResolver) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WasmResolver) ProtoMessage() {}
func (x *WasmResolver) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WasmResolver.ProtoReflect.Descriptor instead.
func (*WasmResolver) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{1}
}
func (x *WasmResolver) GetEntrypoint() string {
if x != nil && x.Entrypoint != nil {
return *x.Entrypoint
}
return ""
}
func (x *WasmResolver) GetModule() string {
if x != nil && x.Module != nil {
return *x.Module
}
return ""
}
func (x *WasmResolver) GetAnnotations() []*Annotations {
if x != nil {
return x.Annotations
}
return nil
}
// Annotations mirrors `ast.Annotations` in v1/ast/annotations.go.
type Annotations struct {
state protoimpl.MessageState `protogen:"open.v1"`
Scope *string `protobuf:"bytes,1,opt,name=scope" json:"scope,omitempty"`
Title *string `protobuf:"bytes,2,opt,name=title" json:"title,omitempty"`
Entrypoint *bool `protobuf:"varint,3,opt,name=entrypoint" json:"entrypoint,omitempty"`
Description *string `protobuf:"bytes,4,opt,name=description" json:"description,omitempty"`
Organizations []string `protobuf:"bytes,5,rep,name=organizations" json:"organizations,omitempty"`
RelatedResources []*RelatedResourceAnnotation `protobuf:"bytes,6,rep,name=related_resources,json=relatedResources" json:"related_resources,omitempty"`
Authors []*AuthorAnnotation `protobuf:"bytes,7,rep,name=authors" json:"authors,omitempty"`
Schemas []*SchemaAnnotation `protobuf:"bytes,8,rep,name=schemas" json:"schemas,omitempty"`
Compile *CompileAnnotation `protobuf:"bytes,9,opt,name=compile" json:"compile,omitempty"`
// `custom` and `labels` are `map[string]any` in Go — genuinely
// free-form, so `Struct` is the right model.
Custom *structpb.Struct `protobuf:"bytes,10,opt,name=custom" json:"custom,omitempty"`
Labels *structpb.Struct `protobuf:"bytes,11,opt,name=labels" json:"labels,omitempty"`
Location *Location `protobuf:"bytes,12,opt,name=location" json:"location,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Annotations) Reset() {
*x = Annotations{}
mi := &file_v1_bundle_manifest_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Annotations) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Annotations) ProtoMessage() {}
func (x *Annotations) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Annotations.ProtoReflect.Descriptor instead.
func (*Annotations) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{2}
}
func (x *Annotations) GetScope() string {
if x != nil && x.Scope != nil {
return *x.Scope
}
return ""
}
func (x *Annotations) GetTitle() string {
if x != nil && x.Title != nil {
return *x.Title
}
return ""
}
func (x *Annotations) GetEntrypoint() bool {
if x != nil && x.Entrypoint != nil {
return *x.Entrypoint
}
return false
}
func (x *Annotations) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
func (x *Annotations) GetOrganizations() []string {
if x != nil {
return x.Organizations
}
return nil
}
func (x *Annotations) GetRelatedResources() []*RelatedResourceAnnotation {
if x != nil {
return x.RelatedResources
}
return nil
}
func (x *Annotations) GetAuthors() []*AuthorAnnotation {
if x != nil {
return x.Authors
}
return nil
}
func (x *Annotations) GetSchemas() []*SchemaAnnotation {
if x != nil {
return x.Schemas
}
return nil
}
func (x *Annotations) GetCompile() *CompileAnnotation {
if x != nil {
return x.Compile
}
return nil
}
func (x *Annotations) GetCustom() *structpb.Struct {
if x != nil {
return x.Custom
}
return nil
}
func (x *Annotations) GetLabels() *structpb.Struct {
if x != nil {
return x.Labels
}
return nil
}
func (x *Annotations) GetLocation() *Location {
if x != nil {
return x.Location
}
return nil
}
// SchemaAnnotation mirrors `ast.SchemaAnnotation`. Path/Schema are
// `ast.Ref` in Go (a list of terms); the wire form is the canonical
// dotted ref string (e.g. `data.foo.bar`). Modeling the term tree
// faithfully would pull most of the AST into this schema and isn't
// worth the cost for annotations.
type SchemaAnnotation struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
Schema *string `protobuf:"bytes,2,opt,name=schema" json:"schema,omitempty"`
// `*any` on the Go side — a parsed JSON Schema document or any
// JSON value. `Value` (not `Struct`) because the top level may be
// a scalar, list, or null, not just an object.
Definition *structpb.Value `protobuf:"bytes,3,opt,name=definition" json:"definition,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SchemaAnnotation) Reset() {
*x = SchemaAnnotation{}
mi := &file_v1_bundle_manifest_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SchemaAnnotation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SchemaAnnotation) ProtoMessage() {}
func (x *SchemaAnnotation) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SchemaAnnotation.ProtoReflect.Descriptor instead.
func (*SchemaAnnotation) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{3}
}
func (x *SchemaAnnotation) GetPath() string {
if x != nil && x.Path != nil {
return *x.Path
}
return ""
}
func (x *SchemaAnnotation) GetSchema() string {
if x != nil && x.Schema != nil {
return *x.Schema
}
return ""
}
func (x *SchemaAnnotation) GetDefinition() *structpb.Value {
if x != nil {
return x.Definition
}
return nil
}
// CompileAnnotation mirrors `ast.CompileAnnotation`. Refs are the
// canonical dotted form; see SchemaAnnotation for the trade-off.
type CompileAnnotation struct {
state protoimpl.MessageState `protogen:"open.v1"`
Unknowns []string `protobuf:"bytes,1,rep,name=unknowns" json:"unknowns,omitempty"`
MaskRule *string `protobuf:"bytes,2,opt,name=mask_rule,json=maskRule" json:"mask_rule,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CompileAnnotation) Reset() {
*x = CompileAnnotation{}
mi := &file_v1_bundle_manifest_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CompileAnnotation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CompileAnnotation) ProtoMessage() {}
func (x *CompileAnnotation) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CompileAnnotation.ProtoReflect.Descriptor instead.
func (*CompileAnnotation) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{4}
}
func (x *CompileAnnotation) GetUnknowns() []string {
if x != nil {
return x.Unknowns
}
return nil
}
func (x *CompileAnnotation) GetMaskRule() string {
if x != nil && x.MaskRule != nil {
return *x.MaskRule
}
return ""
}
// AuthorAnnotation mirrors `ast.AuthorAnnotation`.
type AuthorAnnotation struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Email *string `protobuf:"bytes,2,opt,name=email" json:"email,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthorAnnotation) Reset() {
*x = AuthorAnnotation{}
mi := &file_v1_bundle_manifest_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AuthorAnnotation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AuthorAnnotation) ProtoMessage() {}
func (x *AuthorAnnotation) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AuthorAnnotation.ProtoReflect.Descriptor instead.
func (*AuthorAnnotation) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{5}
}
func (x *AuthorAnnotation) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *AuthorAnnotation) GetEmail() string {
if x != nil && x.Email != nil {
return *x.Email
}
return ""
}
// RelatedResourceAnnotation mirrors `ast.RelatedResourceAnnotation`.
// `Ref` is a `url.URL` in Go, serialized to its `String()` form.
type RelatedResourceAnnotation struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ref *string `protobuf:"bytes,1,opt,name=ref" json:"ref,omitempty"`
Description *string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RelatedResourceAnnotation) Reset() {
*x = RelatedResourceAnnotation{}
mi := &file_v1_bundle_manifest_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RelatedResourceAnnotation) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RelatedResourceAnnotation) ProtoMessage() {}
func (x *RelatedResourceAnnotation) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RelatedResourceAnnotation.ProtoReflect.Descriptor instead.
func (*RelatedResourceAnnotation) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{6}
}
func (x *RelatedResourceAnnotation) GetRef() string {
if x != nil && x.Ref != nil {
return *x.Ref
}
return ""
}
func (x *RelatedResourceAnnotation) GetDescription() string {
if x != nil && x.Description != nil {
return *x.Description
}
return ""
}
// Location mirrors `ast.Location` (= `location.Location`). Only the
// File/Row/Col triple is wire-relevant; `Text`, `Offset`, and `Tabs`
// are tagged `json:"-"` and intentionally absent.
//
// Distinct from `ir.Location`, which is promoted onto the `Stmt`
// envelope in plan.proto. The two are independent Go types.
type Location struct {
state protoimpl.MessageState `protogen:"open.v1"`
File *string `protobuf:"bytes,1,opt,name=file" json:"file,omitempty"`
Row *int32 `protobuf:"varint,2,opt,name=row" json:"row,omitempty"`
Col *int32 `protobuf:"varint,3,opt,name=col" json:"col,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Location) Reset() {
*x = Location{}
mi := &file_v1_bundle_manifest_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Location) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Location) ProtoMessage() {}
func (x *Location) ProtoReflect() protoreflect.Message {
mi := &file_v1_bundle_manifest_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Location.ProtoReflect.Descriptor instead.
func (*Location) Descriptor() ([]byte, []int) {
return file_v1_bundle_manifest_proto_rawDescGZIP(), []int{7}
}
func (x *Location) GetFile() string {
if x != nil && x.File != nil {
return *x.File
}
return ""
}
func (x *Location) GetRow() int32 {
if x != nil && x.Row != nil {
return *x.Row
}
return 0
}
func (x *Location) GetCol() int32 {
if x != nil && x.Col != nil {
return *x.Col
}
return 0
}
var File_v1_bundle_manifest_proto protoreflect.FileDescriptor
const file_v1_bundle_manifest_proto_rawDesc = "" +
"\n" +
"\x18v1/bundle/manifest.proto\x12\ropa.bundle.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x84\x03\n" +
"\bManifest\x12\x1a\n" +
"\brevision\x18\x01 \x01(\tR\brevision\x12\x14\n" +
"\x05roots\x18\x02 \x03(\tR\x05roots\x12/\n" +
"\x04wasm\x18\x03 \x03(\v2\x1b.opa.bundle.v1.WasmResolverR\x04wasm\x12!\n" +
"\frego_version\x18\x04 \x01(\x05R\vregoVersion\x12[\n" +
"\x12file_rego_versions\x18\x05 \x03(\v2-.opa.bundle.v1.Manifest.FileRegoVersionsEntryR\x10fileRegoVersions\x123\n" +
"\bmetadata\x18\x06 \x01(\v2\x17.google.protobuf.StructR\bmetadata\x12\x1b\n" +
"\troots_set\x18\a \x01(\bR\brootsSet\x1aC\n" +
"\x15FileRegoVersionsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\"\x84\x01\n" +
"\fWasmResolver\x12\x1e\n" +
"\n" +
"entrypoint\x18\x01 \x01(\tR\n" +
"entrypoint\x12\x16\n" +
"\x06module\x18\x02 \x01(\tR\x06module\x12<\n" +
"\vannotations\x18\x03 \x03(\v2\x1a.opa.bundle.v1.AnnotationsR\vannotations\"\xc1\x04\n" +
"\vAnnotations\x12\x14\n" +
"\x05scope\x18\x01 \x01(\tR\x05scope\x12\x14\n" +
"\x05title\x18\x02 \x01(\tR\x05title\x12\x1e\n" +
"\n" +
"entrypoint\x18\x03 \x01(\bR\n" +
"entrypoint\x12 \n" +
"\vdescription\x18\x04 \x01(\tR\vdescription\x12$\n" +
"\rorganizations\x18\x05 \x03(\tR\rorganizations\x12U\n" +
"\x11related_resources\x18\x06 \x03(\v2(.opa.bundle.v1.RelatedResourceAnnotationR\x10relatedResources\x129\n" +
"\aauthors\x18\a \x03(\v2\x1f.opa.bundle.v1.AuthorAnnotationR\aauthors\x129\n" +
"\aschemas\x18\b \x03(\v2\x1f.opa.bundle.v1.SchemaAnnotationR\aschemas\x12:\n" +
"\acompile\x18\t \x01(\v2 .opa.bundle.v1.CompileAnnotationR\acompile\x12/\n" +
"\x06custom\x18\n" +
" \x01(\v2\x17.google.protobuf.StructR\x06custom\x12/\n" +
"\x06labels\x18\v \x01(\v2\x17.google.protobuf.StructR\x06labels\x123\n" +
"\blocation\x18\f \x01(\v2\x17.opa.bundle.v1.LocationR\blocation\"v\n" +
"\x10SchemaAnnotation\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x16\n" +
"\x06schema\x18\x02 \x01(\tR\x06schema\x126\n" +
"\n" +
"definition\x18\x03 \x01(\v2\x16.google.protobuf.ValueR\n" +
"definition\"L\n" +
"\x11CompileAnnotation\x12\x1a\n" +
"\bunknowns\x18\x01 \x03(\tR\bunknowns\x12\x1b\n" +
"\tmask_rule\x18\x02 \x01(\tR\bmaskRule\"<\n" +
"\x10AuthorAnnotation\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
"\x05email\x18\x02 \x01(\tR\x05email\"O\n" +
"\x19RelatedResourceAnnotation\x12\x10\n" +
"\x03ref\x18\x01 \x01(\tR\x03ref\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\"B\n" +
"\bLocation\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x10\n" +
"\x03row\x18\x02 \x01(\x05R\x03row\x12\x10\n" +
"\x03col\x18\x03 \x01(\x05R\x03colB3P\x01Z/github.com/open-policy-agent/opa/v1/bundle/v1pbb\beditionsp\xe8\a"
var (
file_v1_bundle_manifest_proto_rawDescOnce sync.Once
file_v1_bundle_manifest_proto_rawDescData []byte
)
func file_v1_bundle_manifest_proto_rawDescGZIP() []byte {
file_v1_bundle_manifest_proto_rawDescOnce.Do(func() {
file_v1_bundle_manifest_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_v1_bundle_manifest_proto_rawDesc), len(file_v1_bundle_manifest_proto_rawDesc)))
})
return file_v1_bundle_manifest_proto_rawDescData
}
var file_v1_bundle_manifest_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_v1_bundle_manifest_proto_goTypes = []any{
(*Manifest)(nil), // 0: opa.bundle.v1.Manifest
(*WasmResolver)(nil), // 1: opa.bundle.v1.WasmResolver
(*Annotations)(nil), // 2: opa.bundle.v1.Annotations
(*SchemaAnnotation)(nil), // 3: opa.bundle.v1.SchemaAnnotation
(*CompileAnnotation)(nil), // 4: opa.bundle.v1.CompileAnnotation
(*AuthorAnnotation)(nil), // 5: opa.bundle.v1.AuthorAnnotation
(*RelatedResourceAnnotation)(nil), // 6: opa.bundle.v1.RelatedResourceAnnotation
(*Location)(nil), // 7: opa.bundle.v1.Location
nil, // 8: opa.bundle.v1.Manifest.FileRegoVersionsEntry
(*structpb.Struct)(nil), // 9: google.protobuf.Struct
(*structpb.Value)(nil), // 10: google.protobuf.Value
}
var file_v1_bundle_manifest_proto_depIdxs = []int32{
1, // 0: opa.bundle.v1.Manifest.wasm:type_name -> opa.bundle.v1.WasmResolver
8, // 1: opa.bundle.v1.Manifest.file_rego_versions:type_name -> opa.bundle.v1.Manifest.FileRegoVersionsEntry
9, // 2: opa.bundle.v1.Manifest.metadata:type_name -> google.protobuf.Struct
2, // 3: opa.bundle.v1.WasmResolver.annotations:type_name -> opa.bundle.v1.Annotations
6, // 4: opa.bundle.v1.Annotations.related_resources:type_name -> opa.bundle.v1.RelatedResourceAnnotation
5, // 5: opa.bundle.v1.Annotations.authors:type_name -> opa.bundle.v1.AuthorAnnotation
3, // 6: opa.bundle.v1.Annotations.schemas:type_name -> opa.bundle.v1.SchemaAnnotation
4, // 7: opa.bundle.v1.Annotations.compile:type_name -> opa.bundle.v1.CompileAnnotation
9, // 8: opa.bundle.v1.Annotations.custom:type_name -> google.protobuf.Struct
9, // 9: opa.bundle.v1.Annotations.labels:type_name -> google.protobuf.Struct
7, // 10: opa.bundle.v1.Annotations.location:type_name -> opa.bundle.v1.Location
10, // 11: opa.bundle.v1.SchemaAnnotation.definition:type_name -> google.protobuf.Value
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_v1_bundle_manifest_proto_init() }
func file_v1_bundle_manifest_proto_init() {
if File_v1_bundle_manifest_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_bundle_manifest_proto_rawDesc), len(file_v1_bundle_manifest_proto_rawDesc)),
NumEnums: 0,
NumMessages: 9,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_v1_bundle_manifest_proto_goTypes,
DependencyIndexes: file_v1_bundle_manifest_proto_depIdxs,
MessageInfos: file_v1_bundle_manifest_proto_msgTypes,
}.Build()
File_v1_bundle_manifest_proto = out.File
file_v1_bundle_manifest_proto_goTypes = nil
file_v1_bundle_manifest_proto_depIdxs = nil
}

View File

@@ -640,7 +640,12 @@ func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment)
if (w.fmtOpts.regoV1 || w.fmtOpts.ifs) && partialSetException {
w.write(" if")
if len(rule.Body) == 1 {
if rule.Body[0].Location.Row == rule.Head.Location.Row {
// Keep `if <term>` on one line when the single body term sits on the
// same line as the end of the head. Comparing against the head's
// start row would wrongly expand the condition into a block whenever
// the head value spans multiple lines (e.g. a multi-line call).
headEndRow := rule.Head.Location.Row + strings.Count(string(rule.Head.Location.Text), "\n")
if rule.Body[0].Location.Row == headEndRow {
w.write(" ")
var err error
comments, err = w.writeExpr(rule.Body[0], comments)

340
vendor/github.com/open-policy-agent/opa/v1/ir/proto.go generated vendored Normal file
View File

@@ -0,0 +1,340 @@
// 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 ir
import (
"fmt"
"math"
"google.golang.org/protobuf/proto"
pb "github.com/open-policy-agent/opa/v1/ir/v1pb"
)
// PolicyToProto converts an IR Policy to its protobuf wire-form,
// defined in v1/ir/plan.proto. Returns an error if the policy contains
// a Stmt or Val kind not yet covered by the encoder switch.
func PolicyToProto(p *Policy) (out *pb.Policy, err error) {
if p == nil {
return nil, nil
}
defer func() {
if r := recover(); r != nil {
out = nil
err = fmt.Errorf("ir: PolicyToProto: %v", r)
}
}()
return &pb.Policy{
Static: staticToProto(p.Static),
Plans: plansToProto(p.Plans),
Funcs: funcsToProto(p.Funcs),
}, nil
}
func staticToProto(s *Static) *pb.Static {
if s == nil {
return nil
}
out := &pb.Static{
Strings: make([]*pb.StringConst, len(s.Strings)),
BuiltinFuncs: make([]*pb.BuiltinFunc, len(s.BuiltinFuncs)),
Files: make([]*pb.StringConst, len(s.Files)),
}
for i, sc := range s.Strings {
out.Strings[i] = stringConstToProto(sc)
}
for i, bf := range s.BuiltinFuncs {
out.BuiltinFuncs[i] = builtinFuncToProto(bf)
}
for i, f := range s.Files {
out.Files[i] = stringConstToProto(f)
}
return out
}
func stringConstToProto(s *StringConst) *pb.StringConst {
if s == nil {
return nil
}
return &pb.StringConst{Value: proto.String(s.Value)}
}
func builtinFuncToProto(b *BuiltinFunc) *pb.BuiltinFunc {
if b == nil {
return nil
}
return &pb.BuiltinFunc{Name: proto.String(b.Name)}
}
func plansToProto(p *Plans) *pb.Plans {
if p == nil {
return nil
}
out := &pb.Plans{Plans: make([]*pb.Plan, len(p.Plans))}
for i, pl := range p.Plans {
out.Plans[i] = planToProto(pl)
}
return out
}
func planToProto(p *Plan) *pb.Plan {
if p == nil {
return nil
}
out := &pb.Plan{Name: proto.String(p.Name), Blocks: make([]*pb.Block, len(p.Blocks))}
for i, b := range p.Blocks {
out.Blocks[i] = blockToProto(b)
}
return out
}
func funcsToProto(f *Funcs) *pb.Funcs {
if f == nil {
return nil
}
out := &pb.Funcs{Funcs: make([]*pb.Func, len(f.Funcs))}
for i, fn := range f.Funcs {
out.Funcs[i] = funcToProto(fn)
}
return out
}
func funcToProto(f *Func) *pb.Func {
if f == nil {
return nil
}
out := &pb.Func{
Name: proto.String(f.Name),
Params: localsToInt32s(f.Params),
Result: proto.Int32(toInt32(f.Return)),
Blocks: make([]*pb.Block, len(f.Blocks)),
Path: f.Path,
}
for i, b := range f.Blocks {
out.Blocks[i] = blockToProto(b)
}
return out
}
func blockToProto(b *Block) *pb.Block {
if b == nil {
return nil
}
out := &pb.Block{Stmts: make([]*pb.Stmt, len(b.Stmts))}
for i, s := range b.Stmts {
out.Stmts[i] = stmtToProto(s)
}
return out
}
func operandToProto(o Operand) *pb.Operand {
return &pb.Operand{Value: valToProto(o.Value)}
}
func operandsToProto(os []Operand) []*pb.Operand {
out := make([]*pb.Operand, len(os))
for i, o := range os {
out[i] = operandToProto(o)
}
return out
}
func valToProto(v Val) *pb.Val {
if v == nil {
return nil
}
switch x := v.(type) {
case Local:
return &pb.Val{Kind: &pb.Val_Local{Local: toInt32(x)}}
case StringIndex:
return &pb.Val{Kind: &pb.Val_StringIndex{StringIndex: toInt32(x)}}
case Bool:
return &pb.Val{Kind: &pb.Val_Bool{Bool: bool(x)}}
default:
panic(fmt.Sprintf("unsupported Val type %T", v))
}
}
// toInt32 narrows an int-based value to int32, panicking if it would
// overflow. PolicyToProto recovers from the panic and returns it as an
// error, so callers don't need to check the bound themselves.
func toInt32[T ~int](v T) int32 {
if int64(v) > math.MaxInt32 || int64(v) < math.MinInt32 {
panic(fmt.Sprintf("value %d overflows int32", int64(v)))
}
return int32(v)
}
func localsToInt32s(ls []Local) []int32 {
out := make([]int32, len(ls))
for i, l := range ls {
out[i] = toInt32(l)
}
return out
}
func intsToInt32s(is []int) []int32 {
out := make([]int32, len(is))
for i, v := range is {
out[i] = toInt32(v)
}
return out
}
func stmtToProto(s Stmt) *pb.Stmt {
if s == nil {
return nil
}
loc := s.GetLocation()
out := &pb.Stmt{
File: proto.Int32(toInt32(loc.File)),
Col: proto.Int32(toInt32(loc.Col)),
Row: proto.Int32(toInt32(loc.Row)),
}
switch x := s.(type) {
case *ArrayAppendStmt:
out.Kind = &pb.Stmt_ArrayAppendStmt{ArrayAppendStmt: &pb.ArrayAppendStmt{
Value: operandToProto(x.Value),
Array: proto.Int32(toInt32(x.Array)),
}}
case *AssignIntStmt:
out.Kind = &pb.Stmt_AssignIntStmt{AssignIntStmt: &pb.AssignIntStmt{
Value: proto.Int64(x.Value),
Target: proto.Int32(toInt32(x.Target)),
}}
case *AssignVarOnceStmt:
out.Kind = &pb.Stmt_AssignVarOnceStmt{AssignVarOnceStmt: &pb.AssignVarOnceStmt{
Source: operandToProto(x.Source),
Target: proto.Int32(toInt32(x.Target)),
}}
case *AssignVarStmt:
out.Kind = &pb.Stmt_AssignVarStmt{AssignVarStmt: &pb.AssignVarStmt{
Source: operandToProto(x.Source),
Target: proto.Int32(toInt32(x.Target)),
}}
case *BlockStmt:
body := &pb.BlockStmt{Blocks: make([]*pb.Block, len(x.Blocks))}
for i, b := range x.Blocks {
body.Blocks[i] = blockToProto(b)
}
out.Kind = &pb.Stmt_BlockStmt{BlockStmt: body}
case *BreakStmt:
out.Kind = &pb.Stmt_BreakStmt{BreakStmt: &pb.BreakStmt{Index: proto.Uint32(x.Index)}}
case *CallDynamicStmt:
out.Kind = &pb.Stmt_CallDynamicStmt{CallDynamicStmt: &pb.CallDynamicStmt{
Args: localsToInt32s(x.Args),
Result: proto.Int32(toInt32(x.Result)),
Path: operandsToProto(x.Path),
}}
case *CallStmt:
out.Kind = &pb.Stmt_CallStmt{CallStmt: &pb.CallStmt{
Function: proto.String(x.Func),
Args: operandsToProto(x.Args),
Result: proto.Int32(toInt32(x.Result)),
}}
case *DotStmt:
out.Kind = &pb.Stmt_DotStmt{DotStmt: &pb.DotStmt{
Source: operandToProto(x.Source),
Key: operandToProto(x.Key),
Target: proto.Int32(toInt32(x.Target)),
}}
case *EqualStmt:
out.Kind = &pb.Stmt_EqualStmt{EqualStmt: &pb.EqualStmt{
A: operandToProto(x.A),
B: operandToProto(x.B),
}}
case *IsArrayStmt:
out.Kind = &pb.Stmt_IsArrayStmt{IsArrayStmt: &pb.IsArrayStmt{Source: operandToProto(x.Source)}}
case *IsDefinedStmt:
out.Kind = &pb.Stmt_IsDefinedStmt{IsDefinedStmt: &pb.IsDefinedStmt{Source: proto.Int32(toInt32(x.Source))}}
case *IsObjectStmt:
out.Kind = &pb.Stmt_IsObjectStmt{IsObjectStmt: &pb.IsObjectStmt{Source: operandToProto(x.Source)}}
case *IsSetStmt:
out.Kind = &pb.Stmt_IsSetStmt{IsSetStmt: &pb.IsSetStmt{Source: operandToProto(x.Source)}}
case *IsUndefinedStmt:
out.Kind = &pb.Stmt_IsUndefinedStmt{IsUndefinedStmt: &pb.IsUndefinedStmt{Source: proto.Int32(toInt32(x.Source))}}
case *LenStmt:
out.Kind = &pb.Stmt_LenStmt{LenStmt: &pb.LenStmt{
Source: operandToProto(x.Source),
Target: proto.Int32(toInt32(x.Target)),
}}
case *MakeArrayStmt:
out.Kind = &pb.Stmt_MakeArrayStmt{MakeArrayStmt: &pb.MakeArrayStmt{
Capacity: proto.Int32(x.Capacity),
Target: proto.Int32(toInt32(x.Target)),
}}
case *MakeNullStmt:
out.Kind = &pb.Stmt_MakeNullStmt{MakeNullStmt: &pb.MakeNullStmt{Target: proto.Int32(toInt32(x.Target))}}
case *MakeNumberIntStmt:
out.Kind = &pb.Stmt_MakeNumberIntStmt{MakeNumberIntStmt: &pb.MakeNumberIntStmt{
Value: proto.Int64(x.Value),
Target: proto.Int32(toInt32(x.Target)),
}}
case *MakeNumberRefStmt:
out.Kind = &pb.Stmt_MakeNumberRefStmt{MakeNumberRefStmt: &pb.MakeNumberRefStmt{
Index: proto.Int32(toInt32(x.Index)),
Target: proto.Int32(toInt32(x.Target)),
}}
case *MakeObjectStmt:
out.Kind = &pb.Stmt_MakeObjectStmt{MakeObjectStmt: &pb.MakeObjectStmt{Target: proto.Int32(toInt32(x.Target))}}
case *MakeSetStmt:
out.Kind = &pb.Stmt_MakeSetStmt{MakeSetStmt: &pb.MakeSetStmt{Target: proto.Int32(toInt32(x.Target))}}
case *NopStmt:
out.Kind = &pb.Stmt_NopStmt{NopStmt: &pb.NopStmt{}}
case *NotEqualStmt:
out.Kind = &pb.Stmt_NotEqualStmt{NotEqualStmt: &pb.NotEqualStmt{
A: operandToProto(x.A),
B: operandToProto(x.B),
}}
case *NotStmt:
out.Kind = &pb.Stmt_NotStmt{NotStmt: &pb.NotStmt{Block: blockToProto(x.Block)}}
case *ObjectInsertOnceStmt:
out.Kind = &pb.Stmt_ObjectInsertOnceStmt{ObjectInsertOnceStmt: &pb.ObjectInsertOnceStmt{
Key: operandToProto(x.Key),
Value: operandToProto(x.Value),
Object: proto.Int32(toInt32(x.Object)),
}}
case *ObjectInsertStmt:
out.Kind = &pb.Stmt_ObjectInsertStmt{ObjectInsertStmt: &pb.ObjectInsertStmt{
Key: operandToProto(x.Key),
Value: operandToProto(x.Value),
Object: proto.Int32(toInt32(x.Object)),
}}
case *ObjectMergeStmt:
out.Kind = &pb.Stmt_ObjectMergeStmt{ObjectMergeStmt: &pb.ObjectMergeStmt{
A: proto.Int32(toInt32(x.A)),
B: proto.Int32(toInt32(x.B)),
Target: proto.Int32(toInt32(x.Target)),
}}
case *ResetLocalStmt:
out.Kind = &pb.Stmt_ResetLocalStmt{ResetLocalStmt: &pb.ResetLocalStmt{Target: proto.Int32(toInt32(x.Target))}}
case *ResultSetAddStmt:
out.Kind = &pb.Stmt_ResultSetAddStmt{ResultSetAddStmt: &pb.ResultSetAddStmt{Value: proto.Int32(toInt32(x.Value))}}
case *ReturnLocalStmt:
out.Kind = &pb.Stmt_ReturnLocalStmt{ReturnLocalStmt: &pb.ReturnLocalStmt{Source: proto.Int32(toInt32(x.Source))}}
case *ScanStmt:
out.Kind = &pb.Stmt_ScanStmt{ScanStmt: &pb.ScanStmt{
Source: proto.Int32(toInt32(x.Source)),
Key: proto.Int32(toInt32(x.Key)),
Value: proto.Int32(toInt32(x.Value)),
Block: blockToProto(x.Block),
}}
case *SetAddStmt:
out.Kind = &pb.Stmt_SetAddStmt{SetAddStmt: &pb.SetAddStmt{
Value: operandToProto(x.Value),
Set: proto.Int32(toInt32(x.Set)),
}}
case *WithStmt:
out.Kind = &pb.Stmt_WithStmt{WithStmt: &pb.WithStmt{
Local: proto.Int32(toInt32(x.Local)),
Path: intsToInt32s(x.Path),
Value: operandToProto(x.Value),
Block: blockToProto(x.Block),
}}
default:
panic(fmt.Sprintf("unsupported Stmt type %T", s))
}
return out
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,9 @@ type Config struct {
// Equal returns true if this key config is equal to the other.
func (k *Config) Equal(other *Config) bool {
if k == other {
return true
}
return other != nil && *k == *other
}

View File

@@ -1,87 +0,0 @@
// 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 logging
import (
"context"
"log/slog"
)
// AsSlogLogger returns a *slog.Logger that forwards log records to the given Logger.
// Structured fields are forwarded via WithFields; log levels map to the equivalent
// Logger methods. Groups are not supported and are ignored.
func AsSlogLogger(logger Logger) *slog.Logger {
return slog.New(slogHandler{logger: logger})
}
// AsSlogLoggerWithPinnedLevel returns a *slog.Logger that forwards all log records
// to the given Logger at the specified fixed level, ignoring the slog record's own level.
// This is useful when bridging a library that uses slog into OPA's logger at a fixed verbosity.
func AsSlogLoggerWithPinnedLevel(logger Logger, level Level) *slog.Logger {
return slog.New(slogHandler{logger: logger, pinnedLevel: &level})
}
type slogHandler struct {
logger Logger
pinnedLevel *Level
}
func (slogHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h slogHandler) Handle(_ context.Context, r slog.Record) error {
fields := make(map[string]any, r.NumAttrs())
r.Attrs(func(a slog.Attr) bool {
fields[a.Key] = a.Value.Any()
return true
})
l := h.logger.WithFields(fields)
if h.pinnedLevel != nil {
dispatchAtLevel(l, *h.pinnedLevel, r.Message)
return nil
}
// slog.Level is int; cascade from most-severe so custom levels between named ones route correctly.
switch {
case r.Level >= slog.LevelError:
l.Error(r.Message)
case r.Level >= slog.LevelWarn:
l.Warn(r.Message)
case r.Level >= slog.LevelInfo:
l.Info(r.Message)
default:
l.Debug(r.Message)
}
return nil
}
func (h slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
fields := make(map[string]any, len(attrs))
for _, a := range attrs {
fields[a.Key] = a.Value.Any()
}
return slogHandler{logger: h.logger.WithFields(fields), pinnedLevel: h.pinnedLevel}
}
func (h slogHandler) WithGroup(_ string) slog.Handler { return h }
func dispatchAtLevel(l Logger, level Level, msg string) {
switch level {
case Error:
l.Error(msg)
case Warn:
l.Warn(msg)
case Info:
l.Info(msg)
default:
l.Debug(msg)
}
}

View File

@@ -6,11 +6,10 @@
package ptr
import (
"strconv"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/storage/internal/errors"
"github.com/open-policy-agent/opa/v1/util"
)
func Ptr(data any, path storage.Path) (any, error) {
@@ -92,7 +91,7 @@ func ValuePtr(data ast.Value, path storage.Path) (ast.Value, error) {
}
func ValidateArrayIndex(arr []any, s string, path storage.Path) (int, error) {
idx, ok := isInt(s)
idx, ok := util.Atoi(s)
if !ok {
return 0, errors.NewNotFoundErrorWithHint(path, errors.ArrayIndexTypeMsg)
}
@@ -100,7 +99,7 @@ func ValidateArrayIndex(arr []any, s string, path storage.Path) (int, error) {
}
func ValidateASTArrayIndex(arr *ast.Array, s string, path storage.Path) (int, error) {
idx, ok := isInt(s)
idx, ok := util.Atoi(s)
if !ok {
return 0, errors.NewNotFoundErrorWithHint(path, errors.ArrayIndexTypeMsg)
}
@@ -111,18 +110,13 @@ func ValidateASTArrayIndex(arr *ast.Array, s string, path storage.Path) (int, er
// array element like `ValidateArrayIndex`, but returns a `resource_conflict` error
// if it is not.
func ValidateArrayIndexForWrite(arr []any, s string, i int, path storage.Path) (int, error) {
idx, ok := isInt(s)
idx, ok := util.Atoi(s)
if !ok {
return 0, errors.NewWriteConflictError(path[:i-1])
}
return inRange(idx, arr, path)
}
func isInt(s string) (int, bool) {
idx, err := strconv.Atoi(s)
return idx, err == nil
}
func inRange(i int, arr any, path storage.Path) (int, error) {
var arrLen int

View File

@@ -25,6 +25,44 @@ func builtinCount(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
return builtins.NewOperandTypeErr(1, operands[0].Value, "array", "object", "set", "string")
}
// termIterable is satisfied by both *ast.Array and ast.Set.
type termIterable interface {
Iter(func(*ast.Term) error) error
}
// exactIntAccumulate accumulates the numbers in a with op on exact big.Ints, reporting false if
// any element is not an integer, in which case the caller falls back to the float path.
//
// That float path accumulates in a big.Float carrying the default mantissa, so integers needing
// more significant bits are silently rounded.
func exactIntAccumulate(a termIterable, init int64, op func(z, x, y *big.Int) *big.Int) (ast.Number, bool) {
acc := big.NewInt(init)
exact := true
_ = a.Iter(func(x *ast.Term) error {
if !exact {
return nil
}
n, ok := x.Value.(ast.Number)
if !ok {
exact = false
return nil
}
i, err := builtins.NumberToInt(n)
if err != nil {
exact = false
return nil
}
op(acc, acc, i)
return nil
})
if !exact {
return "", false
}
return builtins.IntToNumber(acc), true
}
func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
switch a := operands[0].Value.(type) {
case *ast.Array:
@@ -44,6 +82,10 @@ func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
}
// Non-integer values found, so we need to sum as floats.
if n, ok := exactIntAccumulate(a, 0, (*big.Int).Add); ok {
return iter(ast.NewTerm(n))
}
sum := big.NewFloat(0)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
@@ -74,6 +116,10 @@ func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
return iter(ast.InternedTerm(is))
}
if n, ok := exactIntAccumulate(a, 0, (*big.Int).Add); ok {
return iter(ast.NewTerm(n))
}
sum := big.NewFloat(0)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
@@ -95,6 +141,10 @@ func builtinSum(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
func builtinProduct(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
switch a := operands[0].Value.(type) {
case *ast.Array:
if n, ok := exactIntAccumulate(a, 1, (*big.Int).Mul); ok {
return iter(ast.NewTerm(n))
}
product := big.NewFloat(1)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
@@ -110,6 +160,10 @@ func builtinProduct(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
}
return iter(ast.NewTerm(builtins.FloatToNumber(product)))
case ast.Set:
if n, ok := exactIntAccumulate(a, 1, (*big.Int).Mul); ok {
return iter(ast.NewTerm(n))
}
product := big.NewFloat(1)
tmp := new(big.Float)
err := a.Iter(func(x *ast.Term) error {
@@ -260,34 +314,29 @@ func builtinAny(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
}
func builtinMember(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
containee := operands[0]
switch c := operands[1].Value.(type) {
case ast.Set:
return iter(ast.InternedTerm(c.Contains(containee)))
return iter(ast.InternedTerm(c.Contains(operands[0])))
case *ast.Array:
for i := range c.Len() {
if c.Elem(i).Value.Compare(containee.Value) == 0 {
return iter(ast.InternedTerm(true))
}
}
return iter(ast.InternedTerm(false))
return iter(ast.InternedTerm(c.Until(operands[0].Equal)))
case ast.Object:
return iter(ast.InternedTerm(c.Until(func(_, v *ast.Term) bool {
return v.Value.Compare(containee.Value) == 0
return operands[0].Equal(v)
})))
}
return iter(ast.InternedTerm(false))
}
func builtinMemberWithKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
key, val := operands[0], operands[1]
switch c := operands[2].Value.(type) {
case interface{ Get(*ast.Term) *ast.Term }:
ret := false
if act := c.Get(key); act != nil {
ret = act.Value.Compare(val.Value) == 0
}
return iter(ast.InternedTerm(ret))
type getter interface {
Get(*ast.Term) *ast.Term
}
col, key, val := operands[2], operands[0], operands[1]
switch c := col.Value.(type) {
case ast.Set:
return iter(ast.InternedTerm(c.Contains(key) && key.Equal(val)))
case getter:
return iter(ast.InternedTerm(val.Equal(c.Get(key))))
}
return iter(ast.InternedTerm(false))
}

View File

@@ -53,6 +53,22 @@ func arithFloor(a *big.Float) (*big.Float, error) {
return new(big.Float).Sub(f, big.NewFloat(1.0)), nil
}
// exactIntArith applies op to n1 and n2 as exact big.Ints when both are integers.
//
// The big.Float path used otherwise carries the default mantissa, so integers needing more
// significant bits than that are silently rounded before the operation is applied.
func exactIntArith(n1, n2 ast.Number, op func(z, x, y *big.Int) *big.Int) (ast.Number, bool) {
x, err := builtins.NumberToInt(n1)
if err != nil {
return "", false
}
y, err := builtins.NumberToInt(n2)
if err != nil {
return "", false
}
return builtins.IntToNumber(op(new(big.Int), x, y)), true
}
func builtinPlus(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
n1, err := builtins.NumberOperand(operands[0].Value, 1)
if err != nil {
@@ -70,6 +86,10 @@ func builtinPlus(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) er
return iter(ast.InternedTerm(x + y))
}
if n, ok := exactIntArith(n1, n2, (*big.Int).Add); ok {
return iter(ast.NewTerm(n))
}
f := new(big.Float).Add(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
return iter(ast.NewTerm(builtins.FloatToNumber(f)))
@@ -92,6 +112,10 @@ func builtinMultiply(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term
return iter(ast.InternedTerm(x * y))
}
if n, ok := exactIntArith(n1, n2, (*big.Int).Mul); ok {
return iter(ast.NewTerm(n))
}
f := new(big.Float).Mul(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
return iter(ast.NewTerm(builtins.FloatToNumber(f)))
@@ -158,6 +182,10 @@ func builtinMinus(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
return iter(ast.InternedTerm(x - y))
}
if n, ok := exactIntArith(n1, n2, (*big.Int).Sub); ok {
return iter(ast.NewTerm(n))
}
f := new(big.Float).Sub(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
return iter(ast.NewTerm(builtins.FloatToNumber(f)))

View File

@@ -269,12 +269,19 @@ func FloatToNumber(f *big.Float) ast.Number {
// NumberToInt converts n to a big int.
// If n cannot be converted to an big int, an error is returned.
func NumberToInt(n ast.Number) (*big.Int, error) {
f := NumberToFloat(n)
r, accuracy := f.Int(nil)
if accuracy != big.Exact {
// Integer literals are parsed exactly. Going through NumberToFloat first would round any
// value needing more than the big.Float default mantissa, and because the rounded value is
// itself an integer, the accuracy check below cannot detect that it happened.
if i, ok := new(big.Int).SetString(string(n), 10); ok {
return i, nil
}
// Fractional and exponent forms. big.Rat parses both exactly, so a value such as 1e30 stays
// exact, and a genuinely fractional value is rejected rather than silently truncated.
r, ok := new(big.Rat).SetString(string(n))
if !ok || !r.IsInt() {
return nil, errors.New("illegal value")
}
return r, nil
return new(big.Int).Set(r.Num()), nil
}
// IntToNumber converts i to a number.

View File

@@ -41,7 +41,7 @@ type BaseCache interface {
}
type virtualCache struct {
stack []*virtualCacheElem
stack util.SliceStack[*virtualCacheElem]
}
type virtualCacheElem struct {
@@ -57,11 +57,11 @@ func NewVirtualCache() VirtualCache {
}
func (c *virtualCache) Push() {
c.stack = append(c.stack, newVirtualCacheElem())
c.stack.Push(newVirtualCacheElem())
}
func (c *virtualCache) Pop() {
c.stack = c.stack[:len(c.stack)-1]
c.stack.Pop()
}
// Returns the resolved value of the AST term and a flag indicating if the value
@@ -72,7 +72,7 @@ func (c *virtualCache) Pop() {
// nil, false indicates the ref has not been cached
// ast.Term, true is impossible
func (c *virtualCache) Get(ref ast.Ref) (*ast.Term, bool) {
node := c.stack[len(c.stack)-1]
node := c.stack.Peek()
for i := range ref {
x, ok := node.children.Get(ref[i])
if !ok {
@@ -90,7 +90,7 @@ func (c *virtualCache) Get(ref ast.Ref) (*ast.Term, bool) {
// If value is a nil pointer, set the 'undefined' flag on the cache element to
// indicate that the Ref has resolved to undefined.
func (c *virtualCache) Put(ref ast.Ref, value *ast.Term) {
node := c.stack[len(c.stack)-1]
node := c.stack.Peek()
for i := range ref {
x, ok := node.children.Get(ref[i])
if ok {
@@ -109,7 +109,7 @@ func (c *virtualCache) Put(ref ast.Ref, value *ast.Term) {
}
func (c *virtualCache) Keys() []ast.Ref {
node := c.stack[len(c.stack)-1]
node := c.stack.Peek()
return keysRecursive(nil, node)
}
@@ -133,7 +133,7 @@ func newVirtualCacheElem() *virtualCacheElem {
}
func newVirtualCacheHashMap() *util.HasherMap[*ast.Term, *virtualCacheElem] {
return util.NewHasherMap[*ast.Term, *virtualCacheElem](ast.TermValueEqual)
return util.NewHasherMap[*ast.Term, *virtualCacheElem]((*ast.Term).Equal)
}
// baseCache implements a trie structure to cache base documents read out of
@@ -204,7 +204,7 @@ func (e *baseCacheElem) set(value ast.Value) {
}
type refStack struct {
sl []refStackElem
sl util.SliceStack[refStackElem]
}
type refStackElem struct {
@@ -216,20 +216,21 @@ func newRefStack() *refStack {
}
func (s *refStack) Push(refs []ast.Ref) {
s.sl = append(s.sl, refStackElem{refs: refs})
s.sl.Push(refStackElem{refs: refs})
}
func (s *refStack) Pop() {
if s == nil {
return
}
s.sl = s.sl[:len(s.sl)-1]
s.sl.Pop()
}
func (s *refStack) Prefixed(ref ast.Ref) bool {
if s != nil {
for i := len(s.sl) - 1; i >= 0; i-- {
if slices.ContainsFunc(s.sl[i].refs, ref.HasPrefix) {
sl := s.sl.Slice()
for i := len(sl) - 1; i >= 0; i-- {
if slices.ContainsFunc(sl[i].refs, ref.HasPrefix) {
return true
}
}
@@ -238,7 +239,7 @@ func (s *refStack) Prefixed(ref ast.Ref) bool {
}
type comprehensionCache struct {
stack []map[*ast.Term]*comprehensionCacheElem
stack util.SliceStack[map[*ast.Term]*comprehensionCacheElem]
}
type comprehensionCacheElem struct {
@@ -253,20 +254,20 @@ func newComprehensionCache() *comprehensionCache {
}
func (c *comprehensionCache) Push() {
c.stack = append(c.stack, map[*ast.Term]*comprehensionCacheElem{})
c.stack.Push(map[*ast.Term]*comprehensionCacheElem{})
}
func (c *comprehensionCache) Pop() {
c.stack = c.stack[:len(c.stack)-1]
c.stack.Pop()
}
func (c *comprehensionCache) Elem(t *ast.Term) (*comprehensionCacheElem, bool) {
elem, ok := c.stack[len(c.stack)-1][t]
elem, ok := c.stack.Peek()[t]
return elem, ok
}
func (c *comprehensionCache) Set(t *ast.Term, elem *comprehensionCacheElem) {
c.stack[len(c.stack)-1][t] = elem
c.stack.Peek()[t] = elem
}
func newComprehensionCacheElem() *comprehensionCacheElem {
@@ -301,15 +302,13 @@ func (c *comprehensionCacheElem) Put(key []*ast.Term, value *ast.Term) {
}
func newComprehensionCacheHashMap() *util.HasherMap[*ast.Term, *comprehensionCacheElem] {
return util.NewHasherMap[*ast.Term, *comprehensionCacheElem](ast.TermValueEqual)
return util.NewHasherMap[*ast.Term, *comprehensionCacheElem]((*ast.Term).Equal)
}
type functionMocksStack struct {
stack []*functionMocksElem
stack util.GroupStack[frame]
}
type functionMocksElem []frame
type frame map[string]*ast.Term
func newFunctionMocksStack() *functionMocksStack {
@@ -318,21 +317,16 @@ func newFunctionMocksStack() *functionMocksStack {
return stack
}
func newFunctionMocksElem() *functionMocksElem {
return &functionMocksElem{}
}
func (s *functionMocksStack) Push() {
s.stack = append(s.stack, newFunctionMocksElem())
s.stack.PushGroup(nil)
}
func (s *functionMocksStack) Pop() {
s.stack = s.stack[:len(s.stack)-1]
s.stack.PopGroup()
}
func (s *functionMocksStack) PopPairs() {
current := s.stack[len(s.stack)-1]
*current = (*current)[:len(*current)-1]
s.stack.Pop()
}
func (s *functionMocksStack) PutPairs(mocks [][2]*ast.Term) {
@@ -344,8 +338,7 @@ func (s *functionMocksStack) PutPairs(mocks [][2]*ast.Term) {
}
func (s *functionMocksStack) Put(el frame) {
current := s.stack[len(s.stack)-1]
*current = append(*current, el)
s.stack.Push(el)
}
func (s *functionMocksStack) Get(f ast.Ref) (*ast.Term, bool) {
@@ -353,7 +346,7 @@ func (s *functionMocksStack) Get(f ast.Ref) (*ast.Term, bool) {
return nil, false
}
current := *s.stack[len(s.stack)-1]
current := s.stack.PeekGroup()
for i := len(current) - 1; i >= 0; i-- {
if r, ok := current[i][f.String()]; ok {
return r, true

View File

@@ -34,6 +34,9 @@ type CopyPropagator struct {
ensureNonEmptyBody bool
compiler *ast.Compiler
localvargen *localVarGenerator
// placeholders holds vars synthesized to keep a ref alive for its definedness.
// They appear nowhere else, so their bindings can be emitted as the bare ref.
placeholders ast.VarSet
}
type localVarGenerator struct {
@@ -47,10 +50,17 @@ func (l *localVarGenerator) Generate() ast.Var {
}
// generatePlaceholder returns a fresh local variable, recorded as a placeholder.
func (p *CopyPropagator) generatePlaceholder() ast.Var {
v := p.localvargen.Generate()
p.placeholders.Add(v)
return v
}
// New returns a new CopyPropagator that optimizes queries while preserving vars
// in the livevars set.
func New(livevars ast.VarSet) *CopyPropagator {
return &CopyPropagator{livevars: livevars, sorted: util.KeysSorted(livevars), localvargen: &localVarGenerator{}}
return &CopyPropagator{livevars: livevars, sorted: util.KeysSorted(livevars), localvargen: &localVarGenerator{}, placeholders: ast.NewVarSet()}
}
// WithEnsureNonEmptyBody configures p to ensure that results are always non-empty.
@@ -188,7 +198,14 @@ func (p *CopyPropagator) Apply(query ast.Body) ast.Body {
}
if providesSafety || (!safevarRef && !containedIn(b.v, result)) {
result.Append(removedEq)
// For a placeholder key, emit the bare ref rather than `__localcp0__ =
// input.project`: both only require the ref to be defined, but the
// equality leaks the internal var into results (#6378).
if expr := p.placeholderRef(b); expr != nil {
result.Append(expr)
} else {
result.Append(removedEq)
}
safe.Update(outputVars)
}
}
@@ -244,11 +261,10 @@ func (t bindingPlugTransform) Transform(x any) (any, error) {
}
func (bindingPlugTransform) plugBindingsVar(pctx *plugContext, v ast.Var) ast.Value {
var result ast.Value = v
// Apply union-find to remove redundant variables from input.
root, ok := pctx.uf.Find(v)
root, ok := pctx.uf.Find(result)
if ok {
result = root.Value()
}
@@ -258,7 +274,7 @@ func (bindingPlugTransform) plugBindingsVar(pctx *plugContext, v ast.Var) ast.Va
if !ok {
return result
}
b := pctx.removedEqs.Get(v)
b := pctx.removedEqs.Get(result)
if b == nil {
return result
}
@@ -266,7 +282,7 @@ func (bindingPlugTransform) plugBindingsVar(pctx *plugContext, v ast.Var) ast.Va
return result
}
if r, ok := b.(ast.Ref); ok && r.OutputVars().Contains(v) {
if ast.NewTerm(b).Vars().Contains(v) {
return result
}
@@ -311,7 +327,7 @@ func (p *CopyPropagator) updateBindings(pctx *plugContext, expr *ast.Expr) bool
a, b := expr.Operand(0), expr.Operand(1)
if a.Equal(b) {
if p.livevarRef(a) {
pctx.removedEqs.Put(p.localvargen.Generate(), a.Value)
pctx.removedEqs.Put(p.generatePlaceholder(), a.Value)
}
return false
}
@@ -351,6 +367,20 @@ func (p *CopyPropagator) livevarRef(a *ast.Term) bool {
return false
}
// placeholderRef returns the ref a placeholder binding maps to, wrapped as a
// bare expression, or nil if b is not a placeholder-to-ref binding.
func (p *CopyPropagator) placeholderRef(b *binding) *ast.Expr {
k, ok := b.k.(ast.Var)
if !ok || !p.placeholders.Contains(k) {
return nil
}
ref, ok := b.v.(ast.Ref)
if !ok {
return nil
}
return ast.NewExpr(ast.NewTerm(ref))
}
func (p *CopyPropagator) updateBindingsEq(a, b *ast.Term) (ast.Var, ast.Value, bool) {
k, v, keep := p.updateBindingsEqAsymmetric(a, b)
if !keep {
@@ -498,18 +528,20 @@ func makeDisjointSets(livevars ast.VarSet, query ast.Body) (*unionFind, bool) {
func isNoop(expr *ast.Expr) bool {
if !expr.IsCall() && !expr.IsEvery() {
term := expr.Terms.(*ast.Term)
if !ast.IsConstant(term.Value) {
switch t := expr.Terms.(type) {
case []*ast.Term:
// A==A can be ignored
if expr.Operator().Equal(ast.Equal.Ref()) {
return expr.Operand(0).Equal(expr.Operand(1))
}
return false
case *ast.Term:
if !ast.IsConstant(t.Value) {
return false
}
return !ast.Boolean(false).Equal(term.Value)
return !ast.Boolean(false).Equal(t.Value)
default:
// *ast.Every, *ast.Not, *ast.LogicalAnd, *ast.LogicalOr — none are no-ops.
return false
}
// A==A can be ignored
if expr.Operator().Equal(ast.Equal.Ref()) {
return expr.Operand(0).Equal(expr.Operand(1))
}
return false
}

View File

@@ -0,0 +1,24 @@
//go:build go1.27
package topdown
import (
"encoding/json/jsontext"
"errors"
"github.com/open-policy-agent/opa/internal/jsonv2"
)
func (e *Error) MarshalJSONTo(enc *jsontext.Encoder) (err error) {
enc.WriteToken(jsontext.BeginObject)
enc.WriteToken(jsontext.String("code"))
enc.WriteToken(jsontext.String(e.Code))
enc.WriteToken(jsontext.String("message"))
enc.WriteToken(jsontext.String(e.Message))
if e.Location != nil {
err = jsonv2.WriteField(enc, "location", e.Location)
}
return errors.Join(err, enc.WriteToken(jsontext.EndObject))
}

View File

@@ -8,7 +8,6 @@ import (
"slices"
"strconv"
"strings"
"sync"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/metrics"
@@ -130,52 +129,13 @@ type eval struct {
responseMetadata map[string]any
}
type (
evfp struct{ pool sync.Pool }
evbp struct{ pool sync.Pool }
)
func (ep *evfp) Put(e *evalFunc) {
if e != nil {
e.e, e.terms, e.ir = nil, nil, nil
ep.pool.Put(e)
}
}
func (ep *evfp) Get() *evalFunc {
return ep.pool.Get().(*evalFunc)
}
func (ep *evbp) Put(e *evalBuiltin) {
if e != nil {
e.e, e.bi, e.bctx, e.f, e.terms = nil, nil, nil, nil, nil
ep.pool.Put(e)
}
}
func (ep *evbp) Get() *evalBuiltin {
return ep.pool.Get().(*evalBuiltin)
}
var (
evalPool = util.NewSyncPool[eval]()
deecPool = util.NewSyncPool[deferredEarlyExitContainer]()
resolverPool = util.NewSyncPool[evalResolver]()
arraysRecPool = util.NewSyncPool[biunifyArraysRecParams]()
evalFuncPool = &evfp{
pool: sync.Pool{
New: func() any {
return &evalFunc{}
},
},
}
evalBuiltinPool = &evbp{
pool: sync.Pool{
New: func() any {
return &evalBuiltin{}
},
},
}
evalPool = util.NewSyncPool[eval]()
deecPool = util.NewSyncPool[deferredEarlyExitContainer]()
resolverPool = util.NewSyncPool[evalResolver]()
arraysRecPool = util.NewSyncPool[biunifyArraysRecParams]()
evalFuncPool = util.NewResettablePool[evalFunc, *evalFunc]()
evalBuiltinPool = util.NewResettablePool[evalBuiltin, *evalBuiltin]()
)
func (e *eval) Run(iter evalIterator) error {
@@ -545,6 +505,30 @@ func (e *eval) evalStep(iter evalIterator) error {
return err
})
case *ast.LogicalAnd:
ea := evalLogicalAnd{
e: e,
and: terms,
}
err = ea.eval(func(e *eval) error {
defined = true
err := iter(e)
e.traceRedo(expr)
return err
})
case *ast.LogicalOr:
eo := evalLogicalOr{
e: e,
or: terms,
}
err = eo.eval(func(e *eval) error {
defined = true
err := iter(e)
e.traceRedo(expr)
return err
})
default: // guard-rail for adding extra (Expr).Terms types
return fmt.Errorf("got %T terms: %[1]v", terms)
}
@@ -604,6 +588,24 @@ func (e *eval) evalStep(iter evalIterator) error {
return iter(e)
})
case *ast.LogicalAnd:
ea := evalLogicalAnd{
e: e,
and: terms,
}
err = ea.eval(func(e *eval) error {
return iter(e)
})
case *ast.LogicalOr:
eo := evalLogicalOr{
e: e,
or: terms,
}
err = eo.eval(func(e *eval) error {
return iter(e)
})
default: // guard-rail for adding extra (Expr).Terms types
return fmt.Errorf("got %T terms: %[1]v", terms)
}
@@ -2063,6 +2065,12 @@ type evalBuiltin struct {
terms []*ast.Term
}
// Reset clears the fields before this evalBuiltin is returned to its pool,
// so pooling it doesn't keep terms/bindings from the previous call alive.
func (e *evalBuiltin) Reset() {
e.e, e.bi, e.bctx, e.f, e.terms = nil, nil, nil, nil, nil
}
// Is this builtin non-deterministic, and did the caller provide an NDBCache?
func (e *evalBuiltin) canUseNDBCache(bi *ast.Builtin) bool {
return bi.Nondeterministic && e.bctx != nil && e.bctx.NDBuiltinCache != nil
@@ -2178,6 +2186,12 @@ type evalFunc struct {
terms []*ast.Term
}
// Reset clears the fields before this evalFunc is returned to its pool,
// so pooling it doesn't keep terms/index results from the previous call alive.
func (e *evalFunc) Reset() {
e.e, e.terms, e.ir = nil, nil, nil
}
func (e *evalFunc) eval(iter unifyIterator) error {
if e.ir.Empty() {
return nil
@@ -2587,30 +2601,104 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error {
externalRef := node.External.Ref
externalIndex := node.External.Index
// Initialize externalTreeStack if needed
if e.e.externalTreeStack == nil {
e.e.externalTreeStack = newExternalTreeStack(e.e)
// For a parametrized (prefix) external source, the leading
// elements of the reference after the prefix are lookup
// parameters rather than tree descents. The source reports how
// many via ParamArity, keyed off the reference tail's shape (so
// one prefix can back an uneven-depth tree). They must be ground;
// the resolved sub-tree is cached under the full reference
// (prefix + params) so distinct parameters do not collide within
// a single evaluation.
arity := 0
if p, ok := externalIndex.(ast.ParametrizedExternalRuleIndex); ok {
arity = p.ParamArity(e.ref[e.pos+1:])
}
var params []ast.Value
cacheRef := externalRef
expand := true
if arity > 0 {
params = make([]ast.Value, 0, arity)
cacheRef = make(ast.Ref, len(externalRef), len(externalRef)+arity)
copy(cacheRef, externalRef)
for i := 1; i <= arity; i++ {
idx := e.pos + i
if idx >= len(e.ref) {
expand = false
break
}
k := e.bindings.Plug(e.ref[idx])
if !k.IsGround() {
expand = false
break
}
params = append(params, k.Value)
cacheRef = append(cacheRef, k)
}
if !expand {
// The parameter key(s) are not ground, so we cannot
// select a concrete sub-source. Under partial evaluation
// the reference is unknown and must be residualized;
// otherwise it is simply undefined and we fall through
// with the bare (rule-less) external node.
if e.e.partial() {
saved := make(ast.Ref, len(e.ref))
for i := range e.ref {
saved[i] = e.bindings.Plug(e.ref[i])
}
return e.e.saveUnify(ast.NewTerm(saved), e.rterm, e.bindings, e.rbindings, iter)
}
}
}
// Check cache first
cachedNode, _, found := e.e.externalTreeStack.findCached(externalRef)
if found {
node = cachedNode
} else {
// Call Tree() and cache the result
e.e.instr.startTimer(evalOpExternalRuleSource)
tree, updatedIndex, err := node.External.Tree(e.e.ctx, e.e.compiler.RuleTree, externalRef, e.e.input, e.e.metrics, e.e.requestMetadata, e.e.responseMetadata)
e.e.instr.stopTimer(evalOpExternalRuleSource)
if err != nil {
return err
if expand {
// Initialize externalTreeStack if needed
if e.e.externalTreeStack == nil {
e.e.externalTreeStack = newExternalTreeStack(e.e)
}
if tree != nil {
if updatedIndex != nil {
externalIndex = updatedIndex
// Check cache first
cachedNode, _, found := e.e.externalTreeStack.findCached(cacheRef)
var tree *ast.TreeNode
if found {
tree = cachedNode
} else {
// Call Tree() and cache the result.
e.e.instr.startTimer(evalOpExternalRuleSource)
// Pass the eval itself as the resolver: it is save-set
// aware, so external sources that opt into
// ExternalSourceOptions.DistinguishAbsentFromUnknown can
// tell references that are unknown under partial evaluation
// apart from references that are simply absent from the
// concrete input. The parameter terms (params) select the
// concrete sub-source for a parametrized prefix.
t, updatedIndex, err := node.External.Tree(e.e.ctx, e.e.compiler.RuleTree, externalRef, params, e.e, e.e.metrics, e.e.requestMetadata, e.e.responseMetadata)
e.e.instr.stopTimer(evalOpExternalRuleSource)
if err != nil {
return err
}
if t != nil {
if updatedIndex != nil {
externalIndex = updatedIndex
}
e.e.externalTreeStack.Push(cacheRef, t, externalIndex, e.e.input)
pushedExternalTree = true
}
tree = t
}
if tree != nil {
if arity > 0 {
// The resolved sub-tree is rooted at the prefix, but
// the walk still has to consume the parameter
// element(s). Re-insert them as ordinary tree levels
// so the descent below (and any further descent into
// rules) proceeds unchanged.
node = wrapExternalParams(cacheRef[len(externalRef):], tree)
} else {
node = tree
}
e.e.externalTreeStack.Push(externalRef, tree, externalIndex, e.e.input)
node = tree
pushedExternalTree = true
}
}
}
@@ -3295,7 +3383,7 @@ func (e evalVirtualPartial) partialEvalSupportRule(rule *ast.Rule, _ ast.Ref) (b
head.Key = ruleRef[len(ruleRef)-1]
}
if head.Name.Equal(ast.Var("")) && (len(ruleRef) == 1 || (len(ruleRef) == 2 && rule.Head.RuleKind() == ast.SingleValue)) {
if head.Name == "" && (len(ruleRef) == 1 || (len(ruleRef) == 2 && rule.Head.RuleKind() == ast.SingleValue)) {
head.Name = ruleRef[0].Value.(ast.Var)
}
@@ -4214,7 +4302,7 @@ func (e *evalEvery) save(iter unifyIterator) error {
func (e *evalEvery) plug(expr *ast.Expr) (*ast.Expr, error) {
cpy := expr.Copy()
every := cpy.Terms.(*ast.Every)
if err := e.plugBody(every.Body); err != nil {
if err := plugBody(e.e, every.Body); err != nil {
return nil, err
}
@@ -4225,45 +4313,6 @@ func (e *evalEvery) plug(expr *ast.Expr) (*ast.Expr, error) {
return cpy, nil
}
func (e *evalEvery) plugBody(body ast.Body) error {
for i := range body {
switch t := body[i].Terms.(type) {
case *ast.Term:
plugged, err := e.plugTerm(t)
if err != nil {
return err
}
body[i].Terms = plugged
case []*ast.Term:
for j := 1; j < len(t); j++ { // don't plug operator, t[0]
plugged, err := e.plugTerm(t[j])
if err != nil {
return err
}
t[j] = plugged
}
case *ast.Every:
plugged, err := e.plug(body[i])
if err != nil {
return err
}
body[i] = plugged
case *ast.Not:
if err := e.plugBody(t.Body); err != nil {
return err
}
}
}
return nil
}
func (e *evalEvery) plugTerm(t *ast.Term) (*ast.Term, error) {
if ast.IsComprehension(t.Value) {
return e.e.amendComprehension(t, e.e.bindings)
}
return e.e.bindings.PlugNamespaced(t, e.e.caller.bindings), nil
}
type evalNot struct {
e *eval
not *ast.Not
@@ -4364,6 +4413,212 @@ func (e evalNot) evalPartial(iter evalIterator) error {
return e.e.evalNotPartial(expr, unNegate, ast.Complement, supportTerms, iter)
}
type evalLogicalAnd struct {
e *eval
and *ast.LogicalAnd
}
func (e evalLogicalAnd) eval(iter evalIterator) error {
if e.e.partial() && (e.e.unknown(e.and.Lhs, e.e.bindings) || e.e.unknown(e.and.Rhs, e.e.bindings)) {
return e.evalPartial(iter)
}
lhsDefined, err := evalLogicalOperand(e.e, e.and.Lhs)
if err != nil {
return err
}
if !lhsDefined {
// short-circuit: RHS is not evaluated if LHS is undefined
return nil
}
rhsDefined, err := evalLogicalOperand(e.e, e.and.Rhs)
if err != nil {
return err
}
if !rhsDefined {
return nil
}
return iter(e.e)
}
func (e evalLogicalAnd) evalPartial(iter evalIterator) error {
// Plug and save the expression to produce a valid, but non-optimized PE result
expr := e.e.query[e.e.index]
plugged, err := e.plug(expr)
if err != nil {
return err
}
return e.e.saveExpr(plugged, e.e.bindings, func() error {
return iter(e.e)
})
}
func (e evalLogicalAnd) plug(expr *ast.Expr) (*ast.Expr, error) {
cpy := expr.Copy()
and := cpy.Terms.(*ast.LogicalAnd)
if err := plugBody(e.e, and.Lhs); err != nil {
return nil, err
}
if err := plugBody(e.e, and.Rhs); err != nil {
return nil, err
}
cpy.Terms = and
return cpy, nil
}
type evalLogicalOr struct {
e *eval
or *ast.LogicalOr
}
func (e evalLogicalOr) eval(iter evalIterator) error {
if e.e.partial() && (e.e.unknown(e.or.Lhs, e.e.bindings) || e.e.unknown(e.or.Rhs, e.e.bindings)) {
return e.evalPartial(iter)
}
lhsDefined, err := evalLogicalOperand(e.e, e.or.Lhs)
if err != nil {
return err
}
if lhsDefined {
// short-circuit: RHS is not evaluated if LHS is defined
return iter(e.e)
}
rhsDefined, err := evalLogicalOperand(e.e, e.or.Rhs)
if err != nil {
return err
}
if !rhsDefined {
return nil
}
return iter(e.e)
}
func (e evalLogicalOr) evalPartial(iter evalIterator) error {
// Plug and save the expression to produce a valid, but non-optimized PE result
expr := e.e.query[e.e.index]
plugged, err := e.plug(expr)
if err != nil {
return err
}
return e.e.saveExpr(plugged, e.e.bindings, func() error {
return iter(e.e)
})
}
func (e evalLogicalOr) plug(expr *ast.Expr) (*ast.Expr, error) {
cpy := expr.Copy()
or := cpy.Terms.(*ast.LogicalOr)
if err := plugBody(e.e, or.Lhs); err != nil {
return nil, err
}
if err := plugBody(e.e, or.Rhs); err != nil {
return nil, err
}
cpy.Terms = or
return cpy, nil
}
// evalLogicalOperand runs body as a closed scope that contributes at most one
// success. Returns whether the body succeeded; bindings introduced inside body
// do not propagate to the caller.
func evalLogicalOperand(parent *eval, body ast.Body) (bool, error) {
child := evalPool.Get()
defer evalPool.Put(child)
parent.closure(body, child)
child.findOne = true
if parent.traceEnabled {
child.traceEnter(body)
}
defined := false
err := child.eval(func(*eval) error {
if parent.traceEnabled {
child.traceExit(body)
child.traceRedo(body)
}
defined = true
return nil
})
// findOne raises an earlyExitError once the iter callback fires; that's
// our signal to stop, not an error to propagate to the caller.
if err := suppressEarlyExit(err); err != nil {
return false, err
}
return defined, nil
}
func plugBody(e *eval, body ast.Body) error {
for i := range body {
switch t := body[i].Terms.(type) {
case *ast.Term:
plugged, err := plugTerm(e, t)
if err != nil {
return err
}
body[i].Terms = plugged
case []*ast.Term:
for j := 1; j < len(t); j++ { // don't plug operator, t[0]
plugged, err := plugTerm(e, t[j])
if err != nil {
return err
}
t[j] = plugged
}
case *ast.Every:
ev := evalEvery{e: e, every: t, expr: body[i]}
plugged, err := ev.plug(body[i])
if err != nil {
return err
}
body[i] = plugged
case *ast.Not:
if err := plugBody(e, t.Body); err != nil {
return err
}
case *ast.LogicalAnd:
if err := plugBody(e, t.Lhs); err != nil {
return err
}
if err := plugBody(e, t.Rhs); err != nil {
return err
}
case *ast.LogicalOr:
if err := plugBody(e, t.Lhs); err != nil {
return err
}
if err := plugBody(e, t.Rhs); err != nil {
return err
}
}
}
return nil
}
func plugTerm(e *eval, t *ast.Term) (*ast.Term, error) {
if ast.IsComprehension(t.Value) {
return e.amendComprehension(t, e.bindings)
}
return e.bindings.PlugNamespaced(t, e.caller.bindings), nil
}
func (e *eval) comprehensionIndex(term *ast.Term) *ast.ComprehensionIndex {
if e.queryCompiler != nil {
return e.queryCompiler.ComprehensionIndex(term)
@@ -4518,17 +4773,29 @@ func containsNestedRefOrCall(vis *nestedCheckVisitor, expr *ast.Expr) bool {
}
if n, ok := expr.Terms.(*ast.Not); ok {
for _, nExpr := range n.Body {
if containsNestedRefOrCall(vis, nExpr) {
return true
}
}
return false
return containsNestedRefOrCallInBody(vis, n.Body)
}
if a, ok := expr.Terms.(*ast.LogicalAnd); ok {
return containsNestedRefOrCallInBody(vis, a.Lhs) || containsNestedRefOrCallInBody(vis, a.Rhs)
}
if o, ok := expr.Terms.(*ast.LogicalOr); ok {
return containsNestedRefOrCallInBody(vis, o.Lhs) || containsNestedRefOrCallInBody(vis, o.Rhs)
}
return containsNestedRefOrCallInTerm(vis, expr.Terms.(*ast.Term))
}
func containsNestedRefOrCallInBody(vis *nestedCheckVisitor, body ast.Body) bool {
for _, expr := range body {
if containsNestedRefOrCall(vis, expr) {
return true
}
}
return false
}
func containsNestedRefOrCallInTerm(vis *nestedCheckVisitor, term *ast.Term) bool {
switch v := term.Value.(type) {
case ast.Ref:
@@ -4690,6 +4957,21 @@ func (e *eval) updateSavedMocks(withs []*ast.With) []*ast.With {
return ret
}
// wrapExternalParams re-inserts the parameter levels consumed by a
// parametrized external source (see ast.ParametrizedExternalRuleIndex) on
// top of the resolved sub-tree, which is rooted at the registered prefix. The
// evaluator's descent can then consume the parameter element(s) as ordinary
// tree levels. keys are the ground parameter terms in reference order.
func wrapExternalParams(keys []*ast.Term, tree *ast.TreeNode) *ast.TreeNode {
node := tree
for i := len(keys) - 1; i >= 0; i-- {
node = &ast.TreeNode{
Children: map[ast.Value]*ast.TreeNode{keys[i].Value: node},
}
}
return node
}
// simpleTreeNode provides minimal tree structure for navigation
type simpleTreeNode struct {
tree *ast.TreeNode

View File

@@ -479,7 +479,7 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support []
// Build output from saved expressions.
body := ast.NewBody()
for _, elem := range e.saveStack.Stack[len(e.saveStack.Stack)-1] {
for _, elem := range e.saveStack.Peek() {
body.Append(elem.Plug(e.bindings))
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
// saveSet contains a stack of terms that are considered 'unknown' during
@@ -183,40 +184,33 @@ func (sse *saveSetElem) containsVar(t *ast.Term, b *bindings) bool {
// partially evaluated. In this case, the partially evaluated rule will be
// output in the support module.
type saveStack struct {
Stack []saveStackQuery
Stack util.GroupStack[saveStackElem]
}
func newSaveStack() *saveStack {
return &saveStack{
Stack: []saveStackQuery{
{},
},
}
s := &saveStack{}
s.Stack.PushGroup(nil)
return s
}
func (s *saveStack) PushQuery(query saveStackQuery) {
s.Stack = append(s.Stack, query)
s.Stack.PushGroup(query)
}
func (s *saveStack) PopQuery() saveStackQuery {
last := s.Stack[len(s.Stack)-1]
s.Stack = s.Stack[:len(s.Stack)-1]
return last
return s.Stack.PopGroup()
}
func (s *saveStack) Peek() saveStackQuery {
return s.Stack[len(s.Stack)-1]
return s.Stack.PeekGroup()
}
func (s *saveStack) Push(expr *ast.Expr, b1 *bindings, b2 *bindings) {
idx := len(s.Stack) - 1
s.Stack[idx] = append(s.Stack[idx], saveStackElem{expr, b1, b2})
s.Stack.Push(saveStackElem{expr, b1, b2})
}
func (s *saveStack) Pop() {
idx := len(s.Stack) - 1
query := s.Stack[idx]
s.Stack[idx] = query[:len(query)-1]
s.Stack.Pop()
}
type saveStackQuery []saveStackElem
@@ -298,7 +292,7 @@ func (s *saveSupport) Exists(path ast.Ref) bool {
if len(ruleRef) == 1 {
name := ruleRef[0].Value.(ast.Var)
for _, rule := range module.Rules {
if rule.Head.Name.Equal(name) {
if rule.Head.Name == name {
return true
}
}
@@ -586,7 +580,7 @@ func (i *inliningControl) DisabledVar(v ast.Var, ignoreInternal bool) bool {
}
for _, frame := range i.disable {
if (!frame.internal || !ignoreInternal) && frame.v.Equal(v) {
if (!frame.internal || !ignoreInternal) && frame.v == v {
return true
}
}

View File

@@ -129,11 +129,14 @@ func builtinFormatInt(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
}
var format string
var radix int
switch base {
case ast.Number("2"):
format = "%b"
radix = 2
case ast.Number("8"):
format = "%o"
radix = 8
case ast.Number("10"):
// Fast path: for numbers whose decimal string is already interned (e.g.
// "0""100"), we can skip strconv.ParseInt entirely.
@@ -144,12 +147,23 @@ func builtinFormatInt(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
return iter(ast.InternedIntegerString(i))
}
format = "%d"
radix = 10
case ast.Number("16"):
format = "%x"
radix = 16
default:
return builtins.NewOperandEnumErr(2, "2", "8", "10", "16")
}
// For integer inputs, format the exact big.Int. Routing integers through a
// float (as the fractional path below does) loses precision for values that
// need more than a float64's 53-bit mantissa, e.g. 18446744073709551617.
if i, ok := new(big.Int).SetString(string(input), 10); ok {
return iter(ast.InternedTerm(i.Text(radix)))
}
// Fractional inputs (e.g. 15.9) are truncated toward zero, matching the
// historical behaviour: format_int(15.9, 16) == "f", format_int(-15.9, 16) == "-f".
f := builtins.NumberToFloat(input)
i, _ := f.Int(nil)
@@ -543,6 +557,55 @@ func builtinSplit(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
return iter(ast.ArrayTerm(util.SplitMap(text, delim, ast.InternedTerm)...))
}
func builtinSplitN(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
s, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return err
}
d, err := builtins.StringOperand(operands[1].Value, 2)
if err != nil {
return err
}
n, err := builtins.IntOperand(operands[2].Value, 3)
if err != nil {
return err
}
text, delim := string(s), string(d)
var result []*ast.Term
if n >= 0 {
// n+1 may overflow for very large n; a negative limit means no limit.
limit := n + 1
if limit < 0 {
limit = -1
}
parts := strings.SplitN(text, delim, limit)
end := n
if end > len(parts) {
end = len(parts)
}
result = make([]*ast.Term, end)
for i := range result {
result[i] = ast.InternedTerm(parts[i])
}
} else {
parts := strings.Split(text, delim)
start := len(parts) + n
if start < 0 {
start = 0
}
result = make([]*ast.Term, len(parts)-start)
for i, p := range parts[start:] {
result[i] = ast.InternedTerm(p)
}
}
return iter(ast.ArrayTerm(result...))
}
func builtinReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
s, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
@@ -722,15 +785,15 @@ func builtinSprintf(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
return err
}
astArr, ok := operands[1].Value.(*ast.Array)
if !ok {
return builtins.NewOperandTypeErr(2, operands[1].Value, "array")
a, err := builtins.ArrayOperand(operands[1].Value, 2)
if err != nil {
return err
}
// Optimized path for where sprintf is used as a "to_string" function for
// a single integer, i.e. sprintf("%d", [x]) where x is an integer.
if s == "%d" && astArr.Len() == 1 {
if n, ok := astArr.Elem(0).Value.(ast.Number); ok {
if s == "%d" && a.Len() == 1 {
if n, ok := a.Elem(0).Value.(ast.Number); ok {
if i, ok := n.Int(); ok {
if interned := ast.InternedIntegerString(i); interned != nil {
return iter(interned)
@@ -740,24 +803,35 @@ func builtinSprintf(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
}
}
args := make([]any, astArr.Len())
args := make([]any, a.Len())
for i := range args {
switch v := astArr.Elem(i).Value.(type) {
t := a.Elem(i)
switch v := t.Value.(type) {
case ast.Number:
if n, ok := v.Int(); ok {
args[i] = n
} else if b, ok := new(big.Int).SetString(v.String(), 10); ok {
args[i] = b
} else if f, ok := v.Float64(); ok {
args[i] = f
ns := string(v)
if x, ok := util.Atoi64(ns); ok {
args[i] = x
} else {
args[i] = v.String()
if strings.ContainsRune(ns, '.') {
if f, ok := v.Float64(); ok {
args[i] = f
continue
} else {
args[i] = ns
}
} else {
if b, ok := new(big.Int).SetString(ns, 10); ok {
args[i] = b
} else {
args[i] = ns
}
}
}
case ast.String:
args[i] = string(v)
default:
args[i] = astArr.Elem(i).String()
args[i] = t.Value.String()
}
}
@@ -806,6 +880,7 @@ func init() {
RegisterBuiltinFunc(ast.Upper.Name, builtinUpper)
RegisterBuiltinFunc(ast.Lower.Name, builtinLower)
RegisterBuiltinFunc(ast.Split.Name, builtinSplit)
RegisterBuiltinFunc(ast.SplitN.Name, builtinSplitN)
RegisterBuiltinFunc(ast.Replace.Name, builtinReplace)
RegisterBuiltinFunc(ast.ReplaceN.Name, builtinReplaceN)
RegisterBuiltinFunc(ast.Trim.Name, builtinTrim)

View File

@@ -3,7 +3,14 @@ package topdown
import (
"bytes"
"strings"
"text/template"
// A method-less copy of text/template (see internal/methodlesstemplate). Rego values
// decode to map[string]any/[]any/scalars, which have no methods, so eliding
// method calls is a no-op here; it keeps text/template's evalField
// MethodByName off the reachable graph, which otherwise disables the Go
// linker's method-level dead-code elimination binary-wide (golang/go#72895,
// #7903).
template "github.com/open-policy-agent/opa/internal/methodlesstemplate"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/topdown/builtins"

View File

@@ -598,6 +598,49 @@ type varInfo struct {
col int // 0-indexed column
}
// resolveLocalRef resolves a ground ref whose base is a local variable (e.g. 'tc.data')
// against the given local bindings, returning the selected value. It returns nil if the
// ref can't be resolved: the base isn't a bound local, a path element isn't ground (after
// resolving any variable keys), or the path doesn't exist in the value.
func resolveLocalRef(ref ast.Ref, locals *ast.ValueMap) ast.Value {
if len(ref) < 2 || locals == nil {
return nil
}
base, ok := ref[0].Value.(ast.Var)
if !ok {
return nil
}
baseVal := locals.Get(base)
if baseVal == nil {
return nil
}
path := make(ast.Ref, 0, len(ref)-1)
for _, t := range ref[1:] {
if key, ok := t.Value.(ast.Var); ok {
// A variable key (e.g. 'y[i]') must itself be resolved from the local bindings.
keyVal := locals.Get(key)
if keyVal == nil {
return nil
}
path = append(path, ast.NewTerm(keyVal))
continue
}
if !t.IsGround() {
return nil
}
path = append(path, t)
}
val, err := baseVal.Find(path)
if err != nil {
return nil
}
return val
}
func (v varInfo) Value() string {
if v.val != nil {
return v.val.String()
@@ -691,6 +734,21 @@ func PrettyEvent(w io.Writer, e *Event, opts PrettyEventOpts) error {
case *ast.ArrayComprehension, *ast.SetComprehension, *ast.ObjectComprehension:
// we don't report on the internals of a comprehension, as it's already evaluated, and we won't have the local vars.
return true
case ast.Ref:
// For a ref that selects into a local variable (e.g. 'tc.data'), report the
// selected value rather than only the base variable's (potentially large) value.
// We keep descending (return false) so the base variable is still reported too.
if val := resolveLocalRef(v, e.Locals); val != nil {
info := varInfo{
VarMetadata: VarMetadata{Name: ast.Var(term.Location.Text)},
val: val,
exprLoc: term.Location,
col: term.Location.Col,
}
if existing, exists := exprVars[info.Title()]; !exists || existing.val == nil {
exprVars[info.Title()] = info
}
}
case ast.Var:
var info *varInfo
if meta, ok := e.LocalMetadata[v]; ok {

View File

@@ -19,6 +19,22 @@ const (
objectSort
)
// Or works like [cmp.Or] but allows supplier functions to be tried rather than
// alternative values. This allows deferring computation of the alternatives to
// only when needed.
func Or[T comparable](val T, suppliers ...func() T) T {
var zero T
if val == zero {
for _, f := range suppliers {
if alt := f(); alt != zero {
return alt
}
}
}
return val
}
// SliceLenCompare is a convenience function for comparing / sorting
// slices by their length using the various slices.SortX functions.
func SliceLenCompare[T any, S ~[]T](a, b S) int {

View File

@@ -208,9 +208,11 @@ func NewHasherMap[K Hasher, V any](keq func(K, K) bool) *HasherMap[K, V] {
// Get returns the value for k.
func (h *HasherMap[K, V]) Get(k K) (V, bool) {
for entry := h.table[k.Hash()]; entry != nil; entry = entry.next {
if h.keq(entry.k, k) {
return entry.v, true
if h != nil {
for entry := h.table[k.Hash()]; entry != nil; entry = entry.next {
if h.keq(entry.k, k) {
return entry.v, true
}
}
}
var zero V
@@ -250,11 +252,28 @@ func (h *HasherMap[K, V]) Delete(k K) {
}
}
// Keys returns a slice containing all keys in the HasherMap.
func (h *HasherMap[K, V]) Keys() []K {
if h == nil {
return nil
}
keys := make([]K, 0, h.size)
for _, entry := range h.table {
for ; entry != nil; entry = entry.next {
keys = append(keys, entry.k)
}
}
return keys
}
// Iter invokes the iter function for each element in the HasherMap.
// If the iter function returns true, iteration stops and the return value is true.
// If the iter function never returns true, iteration proceeds through all elements
// and the return value is false.
func (h *HasherMap[K, V]) Iter(iter func(K, V) bool) bool {
if h == nil {
return false
}
for _, entry := range h.table {
for ; entry != nil; entry = entry.next {
if iter(entry.k, entry.v) {
@@ -265,7 +284,10 @@ func (h *HasherMap[K, V]) Iter(iter func(K, V) bool) bool {
return false
}
// Len returns the current size of this HashMap.
// Len returns the current size of this HashMap, or 0 if the HasherMap is nil.
func (h *HasherMap[K, V]) Len() int {
if h == nil {
return 0
}
return h.size
}

View File

@@ -36,6 +36,43 @@ func (p *SyncPool[T]) Put(x *T) {
}
}
// resettable is implemented by *T when used with [ResettablePool], allowing
// pooled values to clear their internal state (e.g. drop pointers so they
// don't outlive their useful life) before being returned to the pool.
type resettable[T any] interface {
*T
Reset()
}
// ResettablePool is like [SyncPool], but for types whose pointer clears its
// own fields via a Reset method before being pooled. Unlike a runtime
// interface check on every Put, the PT type parameter is resolved at compile
// time, so there's no extra dispatch cost over a hand-written pool.
type ResettablePool[T any, PT resettable[T]] struct {
pool sync.Pool
}
func NewResettablePool[T any, PT resettable[T]]() *ResettablePool[T, PT] {
return &ResettablePool[T, PT]{
pool: sync.Pool{
New: func() any {
return new(T)
},
},
}
}
func (p *ResettablePool[T, PT]) Get() *T {
return p.pool.Get().(*T)
}
func (p *ResettablePool[T, PT]) Put(x *T) {
if x != nil {
PT(x).Reset()
p.pool.Put(x)
}
}
// NewPtrSlice returns a slice of pointers to T with length n,
// with only 2 allocations performed no matter the size of n.
// See:
@@ -114,6 +151,58 @@ func AppendInt(buf []byte, n int) []byte {
return strconv.AppendInt(buf, int64(n), 10)
}
// Atoi is a convenience function for [Atoi64] where an int is preferable to an int64.
// See the documentation of [Atoi64] for details on the performance benefits of this
// function over strconv.Atoi.
func Atoi(s string) (int, bool) {
if i, ok := Atoi64(s); ok {
return int(i), true
}
return 0, false
}
// Atoi64 is an alternative implementation of strconv.Atoi which is slightly faster for the
// (for our use case) common case of a successful conversion, and crucially — *much* faster
// for the failure case, as this function allocates nothing for any given input string, while
// strconv.Atoi performs 1-2 allocations on failure in its error handling. The callers in this
// codebase — most notably ast.Number's Int() and Int64() methods — have no interest in the
// details of the failure, and keeping this allocation free means both methods can be used
// not only for conversion, but as a most efficient "IsInt64" check.
func Atoi64(s string) (int64, bool) {
sLen := len(s)
if sLen > 0 {
negative := s[0] == '-'
if negative || s[0] == '+' {
s = s[1:]
sLen--
}
if sLen == 0 || sLen > 19 {
return 0, false
}
var n int64
for _, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return 0, false
}
n = n*10 + int64(ch)
}
if !negative && n < 0 {
return 0, false // overflow
}
if negative {
n = -n
if n > 0 {
return 0, false // underflow
}
}
return n, true
}
return 0, false
}
// 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 {

View File

@@ -111,3 +111,94 @@ func (s *FIFO) Pop() (T, bool) {
func (s *FIFO) Size() int {
return s.size
}
// SliceStack is a generic LIFO stack backed by a slice.
type SliceStack[T any] struct {
s []T
}
// Push adds v to the top of the stack.
func (s *SliceStack[T]) Push(v T) {
s.s = append(s.s, v)
}
// Pop removes and returns the top element of the stack.
// It panics if the stack is empty.
func (s *SliceStack[T]) Pop() T {
idx := len(s.s) - 1
v := s.s[idx]
var zero T
s.s[idx] = zero // avoid retaining a reference to v in the backing array
s.s = s.s[:idx]
return v
}
// Peek returns the top element of the stack without removing it.
// It panics if the stack is empty.
func (s *SliceStack[T]) Peek() T {
return s.s[len(s.s)-1]
}
// PeekPtr returns a pointer to the top element, so callers can mutate it in place.
// It panics if the stack is empty.
func (s *SliceStack[T]) PeekPtr() *T {
return &s.s[len(s.s)-1]
}
// Slice returns the stack's underlying slice, bottom-to-top.
func (s *SliceStack[T]) Slice() []T {
return s.s
}
// Len returns the number of elements on the stack.
func (s *SliceStack[T]) Len() int {
return len(s.s)
}
// GroupStack is a two-level stack: a stack of groups, where each group is a
// slice of T. Whole groups are pushed and popped with PushGroup/PopGroup,
// while individual elements are pushed and popped onto the top group with
// Push/Pop. Element lookups (Peek) always target the top group.
//
// Both levels zero their vacated slots when popping, so a popped group or
// element isn't kept alive by the backing arrays.
type GroupStack[T any] struct {
groups SliceStack[[]T]
}
// PushGroup pushes a new group onto the stack. Pass nil for an empty group.
func (g *GroupStack[T]) PushGroup(group []T) {
g.groups.Push(group)
}
// PopGroup removes and returns the top group. It panics if there are no groups.
func (g *GroupStack[T]) PopGroup() []T {
return g.groups.Pop()
}
// PeekGroup returns the top group without removing it. It panics if there are
// no groups.
func (g *GroupStack[T]) PeekGroup() []T {
return g.groups.Peek()
}
// Push appends v to the top group. It panics if there are no groups.
func (g *GroupStack[T]) Push(v T) {
top := g.groups.PeekPtr()
*top = append(*top, v)
}
// Pop removes the top element of the top group. It panics if there are no
// groups or the top group is empty.
func (g *GroupStack[T]) Pop() {
top := g.groups.PeekPtr()
idx := len(*top) - 1
var zero T
(*top)[idx] = zero // avoid retaining a reference in the backing array
*top = (*top)[:idx]
}
// Len returns the number of groups on the stack.
func (g *GroupStack[T]) Len() int {
return g.groups.Len()
}

View File

@@ -10,7 +10,7 @@ import (
"runtime/debug"
)
var Version = "1.18.2"
var Version = "1.19.0"
// GoVersion is the version of Go this was built with
var GoVersion = runtime.Version()

View File

@@ -32,7 +32,13 @@ type Definition struct {
EnumValues EnumValueList // enum
Position *Position `dump:"-" json:"-"`
BuiltIn bool `dump:"-"`
// TypePositions holds the source position of each Types entry (a union's
// member types), parallel to Types. The parser populates it so that
// validation can point at the offending member; when populated its length
// equals len(Types). It is empty for definitions built programmatically, in
// which case validators fall back to the definition's own Position.
TypePositions []*Position `dump:"-" json:"-"`
BuiltIn bool `dump:"-"`
BeforeDescriptionComment *CommentGroup
AfterDescriptionComment *CommentGroup

View File

@@ -58,7 +58,7 @@ func (d *dumper) dump(v reflect.Value) {
case reflect.Array, reflect.Slice:
d.dumpArray(v)
case reflect.Interface, reflect.Ptr:
case reflect.Interface, reflect.Pointer:
d.dumpPtr(v)
case reflect.Struct:
@@ -79,7 +79,7 @@ func (d *dumper) nl() {
}
func typeName(t reflect.Type) string {
if t.Kind() == reflect.Ptr {
if t.Kind() == reflect.Pointer {
return typeName(t.Elem())
}
return t.Name()
@@ -122,7 +122,7 @@ func (d *dumper) dumpStruct(v reflect.Value) {
func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
case reflect.Pointer, reflect.Interface:
return v.IsNil()
case reflect.Func, reflect.Map:
return v.IsNil()

View File

@@ -193,7 +193,7 @@ func (s *Lexer) ReadToken() (Token, error) {
s.endRunes--
if r < 0x0020 && r != 0x0009 && r != 0x000a && r != 0x000d {
return s.makeError(`Cannot contain the invalid character "\u%04d"`, r)
return s.makeError(`Cannot contain the invalid character "\u%04x"`, r)
}
if r == '\'' {
@@ -365,7 +365,7 @@ func (s *Lexer) readString() (Token, error) {
break
}
if r < 0x0020 && r != '\t' {
return s.makeError(`Invalid character within String: "\u%04d".`, r)
return s.makeError(`Invalid character within String: "\u%04x".`, r)
}
switch r {
default:
@@ -505,7 +505,7 @@ func (s *Lexer) readBlockString() (Token, error) {
// SourceCharacter
if r < 0x0020 && r != '\t' && r != '\n' && r != '\r' {
return s.makeError(`Invalid character within String: "\u%04d".`, r)
return s.makeError(`Invalid character within String: "\u%04x".`, r)
}
switch {

View File

@@ -231,6 +231,12 @@ lex reports useful string errors:
message: 'Invalid character within String: "\u0000".'
locations: [{ line: 1, column: 19 }]
- name: control character codepoint reported in hex
input: "\"contains \u000e sub char\""
error:
message: 'Invalid character within String: "\u000e".'
locations: [{ line: 1, column: 11 }]
- name: unterminated newline
input: "\"multi\nline\""
error:

View File

@@ -329,22 +329,26 @@ func (p *parser) parseUnionTypeDefinition(description descriptionWithComment) *D
def.AfterDescriptionComment = comment
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Types = p.parseUnionMemberTypes()
def.Types, def.TypePositions = p.parseUnionMemberTypes()
return &def
}
func (p *parser) parseUnionMemberTypes() []string {
var types []string
// parseUnionMemberTypes parses a union's member type list. It returns the member
// type names alongside their source positions; the two slices have equal length
// (one position per name), so callers can report errors at a specific member.
func (p *parser) parseUnionMemberTypes() (types []string, positions []*Position) {
if p.skip(lexer.Equals) {
// optional leading pipe
p.skip(lexer.Pipe)
positions = append(positions, p.peekPos())
types = append(types, p.parseName())
for p.skip(lexer.Pipe) && p.err == nil {
positions = append(positions, p.peekPos())
types = append(types, p.parseName())
}
}
return types
return types, positions
}
func (p *parser) parseEnumTypeDefinition(description descriptionWithComment) *Definition {
@@ -506,7 +510,7 @@ func (p *parser) parseUnionTypeExtension(comment *CommentGroup) *Definition {
def.Kind = Union
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
def.Types = p.parseUnionMemberTypes()
def.Types, def.TypePositions = p.parseUnionMemberTypes()
if len(def.Directives) == 0 && len(def.Types) == 0 {
p.unexpectedError()

View File

@@ -62,6 +62,7 @@ func ValidateSchemaDocument(sd *SchemaDocument) (*Schema, error) {
def.Interfaces = append(def.Interfaces, ext.Interfaces...)
def.Fields = append(def.Fields, ext.Fields...)
def.Types = append(def.Types, ext.Types...)
def.TypePositions = append(def.TypePositions, ext.TypePositions...)
def.EnumValues = append(def.EnumValues, ext.EnumValues...)
}
@@ -421,6 +422,29 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
}
}
// Reject duplicate union member types, pointing at the duplicate member's
// position when it is known. TypePositions is parallel to Types (populated by
// the parser); when it is absent or not aligned, fall back to the
// definition's own position.
memberPosAligned := len(def.TypePositions) == len(def.Types)
for i, typ1 := range def.Types {
for j := i + 1; j < len(def.Types); j++ {
if typ1 != def.Types[j] {
continue
}
pos := def.Position
if memberPosAligned && def.TypePositions[j] != nil {
pos = def.TypePositions[j]
}
return gqlerror.ErrorPosf(
pos,
"Union type %s can only include type %s once.",
def.Name,
def.Types[j],
)
}
}
if !def.BuiltIn {
// GraphQL spec has reserved type names a lot!
err := validateName(def.Position, def.Name)

View File

@@ -494,6 +494,37 @@ unions:
error:
message: "UNION type \"Baz\" must be OBJECT."
locations: [{line: 1, column: 7}]
- name: cannot include same union member twice at same definition
input: |
union Foo = Bar | Bar
type Bar {
id: ID
}
error:
message: "Union type Foo can only include type Bar once."
locations: [{line: 1, column: 19}]
- name: cannot include same union member twice across extension
input: |
union Foo = Bar
extend union Foo = Bar
type Bar {
id: ID
}
error:
message: "Union type Foo can only include type Bar once."
locations: [{line: 2, column: 20}]
- name: reports the second occurrence among multiple members
input: |
union Foo = Bar | Baz | Bar
type Bar {
id: ID
}
type Baz {
id: ID
}
error:
message: "Union type Foo can only include type Bar once."
locations: [{line: 1, column: 25}]
- name: unions of pure type extensions are valid
input: |

View File

@@ -86,7 +86,7 @@ func VariableValues(
rv = reflect.ValueOf(f)
}
}
if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
if rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
rv = rv.Elem()
}
@@ -117,6 +117,14 @@ func (v *varValidator) validateVarType(
v.path = currentPath
}
defer resetPath()
if !val.IsValid() {
if typ.NonNull {
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
}
return val, nil
}
if typ.Elem != nil {
if val.Kind() != reflect.Slice {
// GraphQL spec says that non-null values should be coerced to an array when possible.
@@ -129,7 +137,7 @@ func (v *varValidator) validateVarType(
resetPath()
v.path = append(v.path, ast.PathIndex(i))
field := val.Index(i)
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
if field.Kind() == reflect.Pointer || field.Kind() == reflect.Interface {
if typ.Elem.NonNull && field.IsNil() {
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
}
@@ -147,11 +155,6 @@ func (v *varValidator) validateVarType(
panic(fmt.Errorf("missing def for %s", typ.NamedType))
}
if !typ.NonNull && !val.IsValid() {
// If the type is not null and we got a invalid value namely null/nil, then it's valid
return val, nil
}
switch def.Kind {
case ast.Enum:
kind := val.Type().Kind()
@@ -245,7 +248,7 @@ func (v *varValidator) validateVarType(
continue
}
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
if field.Kind() == reflect.Pointer || field.Kind() == reflect.Interface {
if fieldDef.Type.NonNull && field.IsNil() {
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
}

11
vendor/modules.txt vendored
View File

@@ -548,7 +548,7 @@ github.com/go-ldap/ldif
# github.com/go-logfmt/logfmt v0.5.1
## explicit; go 1.17
github.com/go-logfmt/logfmt
# github.com/go-logr/logr v1.4.3
# github.com/go-logr/logr v1.4.4
## explicit; go 1.18
github.com/go-logr/logr
github.com/go-logr/logr/funcr
@@ -1274,7 +1274,7 @@ github.com/onsi/gomega/matchers/support/goraph/edge
github.com/onsi/gomega/matchers/support/goraph/node
github.com/onsi/gomega/matchers/support/goraph/util
github.com/onsi/gomega/types
# github.com/open-policy-agent/opa v1.18.2
# github.com/open-policy-agent/opa v1.19.0
## explicit; go 1.25.0
github.com/open-policy-agent/opa/ast
github.com/open-policy-agent/opa/ast/json
@@ -1294,9 +1294,12 @@ github.com/open-policy-agent/opa/internal/file/url
github.com/open-policy-agent/opa/internal/future
github.com/open-policy-agent/opa/internal/gojsonschema
github.com/open-policy-agent/opa/internal/json/patch
github.com/open-policy-agent/opa/internal/jsonv2
github.com/open-policy-agent/opa/internal/lcss
github.com/open-policy-agent/opa/internal/leb128
github.com/open-policy-agent/opa/internal/merge
github.com/open-policy-agent/opa/internal/methodlesstemplate
github.com/open-policy-agent/opa/internal/methodlesstemplate/internal/fmtsort
github.com/open-policy-agent/opa/internal/planner
github.com/open-policy-agent/opa/internal/providers/aws
github.com/open-policy-agent/opa/internal/providers/aws/crypto
@@ -1325,9 +1328,11 @@ github.com/open-policy-agent/opa/v1/ast/internal/tokens
github.com/open-policy-agent/opa/v1/ast/json
github.com/open-policy-agent/opa/v1/ast/location
github.com/open-policy-agent/opa/v1/bundle
github.com/open-policy-agent/opa/v1/bundle/v1pb
github.com/open-policy-agent/opa/v1/capabilities
github.com/open-policy-agent/opa/v1/format
github.com/open-policy-agent/opa/v1/ir
github.com/open-policy-agent/opa/v1/ir/v1pb
github.com/open-policy-agent/opa/v1/keys
github.com/open-policy-agent/opa/v1/loader
github.com/open-policy-agent/opa/v1/loader/extension
@@ -2122,7 +2127,7 @@ github.com/urfave/cli/v2
## explicit; go 1.24
github.com/valyala/fastjson
github.com/valyala/fastjson/fastfloat
# github.com/vektah/gqlparser/v2 v2.5.34
# github.com/vektah/gqlparser/v2 v2.5.36
## explicit; go 1.22
github.com/vektah/gqlparser/v2/ast
github.com/vektah/gqlparser/v2/gqlerror