build(deps): bump github.com/open-policy-agent/opa from 0.51.0 to 0.59.0
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.51.0 to 0.59.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/v0.51.0...v0.59.0) --- updated-dependencies: - dependency-name: github.com/open-policy-agent/opa dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
committed by
Ralf Haferkamp
parent
a6a6c22c14
commit
1f069c7c00
+15
-11
@@ -11,6 +11,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/internal/deepcopy"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
@@ -39,7 +40,7 @@ type (
|
||||
|
||||
comments []*Comment
|
||||
node Node
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// SchemaAnnotation contains a schema declaration for the document identified by the path.
|
||||
@@ -76,7 +77,7 @@ type (
|
||||
Annotations *Annotations `json:"annotations,omitempty"`
|
||||
Location *Location `json:"location,omitempty"` // The location of the node the annotations are applied to
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
|
||||
node Node // The node the annotations are applied to
|
||||
}
|
||||
@@ -174,14 +175,17 @@ func (a *Annotations) GetTargetPath() Ref {
|
||||
case *Package:
|
||||
return n.Path
|
||||
case *Rule:
|
||||
return n.Path()
|
||||
return n.Ref().GroundPrefix()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Annotations) setJSONOptions(opts JSONOptions) {
|
||||
func (a *Annotations) setJSONOptions(opts astJSON.Options) {
|
||||
a.jsonOptions = opts
|
||||
if a.Location != nil {
|
||||
a.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
@@ -348,12 +352,12 @@ func compareRelatedResources(a, b []*RelatedResourceAnnotation) int {
|
||||
}
|
||||
|
||||
func compareSchemas(a, b []*SchemaAnnotation) int {
|
||||
max := len(a)
|
||||
if len(b) < max {
|
||||
max = len(b)
|
||||
maxLen := len(a)
|
||||
if len(b) < maxLen {
|
||||
maxLen = len(b)
|
||||
}
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
for i := 0; i < maxLen; i++ {
|
||||
if cmp := a[i].Compare(b[i]); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
@@ -715,7 +719,7 @@ func (as *AnnotationSet) add(a *Annotations) *Error {
|
||||
}
|
||||
case annotationScopeDocument:
|
||||
if rule, ok := a.node.(*Rule); ok {
|
||||
path := rule.Path()
|
||||
path := rule.Ref().GroundPrefix()
|
||||
x := as.byPath.get(path)
|
||||
if x != nil {
|
||||
return errAnnotationRedeclared(a, x.Value.Location)
|
||||
@@ -811,7 +815,7 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
|
||||
// Make sure there is always a leading entry representing the passed rule, even if it has no annotations
|
||||
refs = append(refs, &AnnotationsRef{
|
||||
Location: rule.Location,
|
||||
Path: rule.Path(),
|
||||
Path: rule.Ref().GroundPrefix(),
|
||||
node: rule,
|
||||
})
|
||||
}
|
||||
@@ -823,7 +827,7 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
|
||||
})
|
||||
}
|
||||
|
||||
docAnnots := as.GetDocumentScope(rule.Path())
|
||||
docAnnots := as.GetDocumentScope(rule.Ref().GroundPrefix())
|
||||
if docAnnots != nil {
|
||||
refs = append(refs, NewAnnotationsRef(docAnnots))
|
||||
}
|
||||
|
||||
+105
-4
@@ -133,9 +133,11 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
TrimSpace,
|
||||
Sprintf,
|
||||
StringReverse,
|
||||
RenderTemplate,
|
||||
|
||||
// Numbers
|
||||
NumbersRange,
|
||||
NumbersRangeStep,
|
||||
RandIntn,
|
||||
|
||||
// Encoding
|
||||
@@ -210,10 +212,13 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
CryptoSha256,
|
||||
CryptoX509ParseCertificateRequest,
|
||||
CryptoX509ParseRSAPrivateKey,
|
||||
CryptoX509ParseKeyPair,
|
||||
CryptoParsePrivateKeys,
|
||||
CryptoHmacMd5,
|
||||
CryptoHmacSha1,
|
||||
CryptoHmacSha256,
|
||||
CryptoHmacSha512,
|
||||
CryptoHmacEqual,
|
||||
|
||||
// Graphs
|
||||
WalkBuiltin,
|
||||
@@ -282,6 +287,7 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
|
||||
// UUIDs
|
||||
UUIDRFC4122,
|
||||
UUIDParse,
|
||||
|
||||
// SemVers
|
||||
SemVerIsValid,
|
||||
@@ -1312,6 +1318,20 @@ var StringReverse = &Builtin{
|
||||
Categories: stringsCat,
|
||||
}
|
||||
|
||||
var RenderTemplate = &Builtin{
|
||||
Name: "strings.render_template",
|
||||
Description: `Renders a templated string with given template variables injected. For a given templated string and key/value mapping, values will be injected into the template where they are referenced by key.
|
||||
For examples of templating syntax, see https://pkg.go.dev/text/template`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("value", types.S).Description("a templated string"),
|
||||
types.Named("vars", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("a mapping of template variable keys to values"),
|
||||
),
|
||||
types.Named("result", types.S).Description("rendered template with template variables injected"),
|
||||
),
|
||||
Categories: stringsCat,
|
||||
}
|
||||
|
||||
/**
|
||||
* Numbers
|
||||
*/
|
||||
@@ -1344,6 +1364,23 @@ var NumbersRange = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var NumbersRangeStep = &Builtin{
|
||||
Name: "numbers.range_step",
|
||||
Description: `Returns an array of numbers in the given (inclusive) range incremented by a positive step.
|
||||
If "a==b", then "range == [a]"; if "a > b", then "range" is in descending order.
|
||||
If the provided "step" is less then 1, an error will be thrown.
|
||||
If "b" is not in the range of the provided "step", "b" won't be included in the result.
|
||||
`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("a", types.N),
|
||||
types.Named("b", types.N),
|
||||
types.Named("step", types.N),
|
||||
),
|
||||
types.Named("range", types.NewArray(nil, types.N)).Description("the range between `a` and `b` in `step` increments"),
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* Units
|
||||
*/
|
||||
@@ -1397,6 +1434,19 @@ var UUIDRFC4122 = &Builtin{
|
||||
Nondeterministic: true,
|
||||
}
|
||||
|
||||
var UUIDParse = &Builtin{
|
||||
Name: "uuid.parse",
|
||||
Description: "Parses the string value as an UUID and returns an object with the well-defined fields of the UUID if valid.",
|
||||
Categories: nil,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("uuid", types.S),
|
||||
),
|
||||
types.Named("result", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("Properties of UUID if valid (version, variant, etc). Undefined otherwise."),
|
||||
),
|
||||
Relation: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON
|
||||
*/
|
||||
@@ -2158,7 +2208,7 @@ var Format = &Builtin{
|
||||
types.N,
|
||||
types.NewArray([]types.Type{types.N, types.S}, nil),
|
||||
types.NewArray([]types.Type{types.N, types.S, types.S}, nil),
|
||||
)).Description("a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string (see golang supported time formats)"),
|
||||
)).Description("a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string or golang defined formatting constant (see golang supported time formats)"),
|
||||
),
|
||||
types.Named("formatted timestamp", types.S).Description("the formatted timestamp represented for the nanoseconds since the epoch in the supplied timezone (or UTC)"),
|
||||
),
|
||||
@@ -2209,7 +2259,7 @@ var Weekday = &Builtin{
|
||||
|
||||
var AddDate = &Builtin{
|
||||
Name: "time.add_date",
|
||||
Description: "Returns the nanoseconds since epoch after adding years, months and days to nanoseconds. `undefined` if the result would be outside the valid time range that can fit within an `int64`.",
|
||||
Description: "Returns the nanoseconds since epoch after adding years, months and days to nanoseconds. Month & day values outside their usual ranges after the operation and will be normalized - for example, October 32 would become November 1. `undefined` if the result would be outside the valid time range that can fit within an `int64`.",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("ns", types.N).Description("nanoseconds since the epoch"),
|
||||
@@ -2288,6 +2338,17 @@ var CryptoX509ParseCertificateRequest = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoX509ParseKeyPair = &Builtin{
|
||||
Name: "crypto.x509.parse_keypair",
|
||||
Description: "Returns a valid key pair",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("cert", types.S).Description("string containing PEM or base64 encoded DER certificates"),
|
||||
types.Named("pem", types.S).Description("string containing PEM or base64 encoded DER keys"),
|
||||
),
|
||||
types.Named("output", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("if key pair is valid, returns the tls.certificate(https://pkg.go.dev/crypto/tls#Certificate) as an object. If the key pair is invalid, nil and an error are returned."),
|
||||
),
|
||||
}
|
||||
var CryptoX509ParseRSAPrivateKey = &Builtin{
|
||||
Name: "crypto.x509.parse_rsa_private_key",
|
||||
Description: "Returns a JWK for signing a JWT from the given PEM-encoded RSA private key.",
|
||||
@@ -2299,6 +2360,19 @@ var CryptoX509ParseRSAPrivateKey = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoParsePrivateKeys = &Builtin{
|
||||
Name: "crypto.parse_private_keys",
|
||||
Description: `Returns zero or more private keys from the given encoded string containing DER certificate data.
|
||||
|
||||
If the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("keys", types.S).Description("PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded."),
|
||||
),
|
||||
types.Named("output", types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)))).Description("parsed private keys represented as objects"),
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoMd5 = &Builtin{
|
||||
Name: "crypto.md5",
|
||||
Description: "Returns a string representing the input string hashed with the MD5 function",
|
||||
@@ -2380,6 +2454,18 @@ var CryptoHmacSha512 = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoHmacEqual = &Builtin{
|
||||
Name: "crypto.hmac.equal",
|
||||
Description: "Returns a boolean representing the result of comparing two MACs for equality without leaking timing information.",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("mac1", types.S).Description("mac1 to compare"),
|
||||
types.Named("mac2", types.S).Description("mac2 to compare"),
|
||||
),
|
||||
types.Named("result", types.B).Description("`true` if the MACs are equals, `false` otherwise"),
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* Graphs.
|
||||
*/
|
||||
@@ -2399,7 +2485,7 @@ var WalkBuiltin = &Builtin{
|
||||
types.A,
|
||||
},
|
||||
nil,
|
||||
)).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`"),
|
||||
)).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`. If `path` is assigned a wildcard (`_`), the `walk` function will skip path creation entirely for faster evaluation."),
|
||||
),
|
||||
Categories: graphs,
|
||||
}
|
||||
@@ -3155,6 +3241,21 @@ func category(cs ...string) []string {
|
||||
return cs
|
||||
}
|
||||
|
||||
// Minimal returns a shallow copy of b with the descriptions and categories and
|
||||
// named arguments stripped out.
|
||||
func (b *Builtin) Minimal() *Builtin {
|
||||
cpy := *b
|
||||
fargs := b.Decl.FuncArgs()
|
||||
if fargs.Variadic != nil {
|
||||
cpy.Decl = types.NewVariadicFunction(fargs.Args, fargs.Variadic, b.Decl.Result())
|
||||
} else {
|
||||
cpy.Decl = types.NewFunction(fargs.Args, b.Decl.Result())
|
||||
}
|
||||
cpy.Categories = nil
|
||||
cpy.Description = ""
|
||||
return &cpy
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the Builtin function is deprecated and will be removed in a future release.
|
||||
func (b *Builtin) IsDeprecated() bool {
|
||||
return b.deprecated
|
||||
@@ -3201,7 +3302,7 @@ func (b *Builtin) Ref() Ref {
|
||||
// IsTargetPos returns true if a variable in the i-th position will be bound by
|
||||
// evaluating the call expression.
|
||||
func (b *Builtin) IsTargetPos(i int) bool {
|
||||
return len(b.Decl.Args()) == i
|
||||
return len(b.Decl.FuncArgs().Args) == i
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
+105
-4
@@ -6,6 +6,8 @@ package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,27 +15,56 @@ import (
|
||||
"strings"
|
||||
|
||||
caps "github.com/open-policy-agent/opa/capabilities"
|
||||
"github.com/open-policy-agent/opa/internal/semver"
|
||||
"github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
// VersonIndex contains an index from built-in function name, language feature,
|
||||
// and future rego keyword to version number. During the build, this is used to
|
||||
// create an index of the minimum version required for the built-in/feature/kw.
|
||||
type VersionIndex struct {
|
||||
Builtins map[string]semver.Version `json:"builtins"`
|
||||
Features map[string]semver.Version `json:"features"`
|
||||
Keywords map[string]semver.Version `json:"keywords"`
|
||||
}
|
||||
|
||||
// NOTE(tsandall): this file is generated by internal/cmd/genversionindex/main.go
|
||||
// and run as part of go:generate. We generate the version index as part of the
|
||||
// build process because it's relatively expensive to build (it takes ~500ms on
|
||||
// my machine) and never changes.
|
||||
//
|
||||
//go:embed version_index.json
|
||||
var versionIndexBs []byte
|
||||
|
||||
var minVersionIndex = func() VersionIndex {
|
||||
var vi VersionIndex
|
||||
err := json.Unmarshal(versionIndexBs, &vi)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vi
|
||||
}()
|
||||
|
||||
// In the compiler, we used this to check that we're OK working with ref heads.
|
||||
// If this isn't present, we'll fail. This is to ensure that older versions of
|
||||
// OPA can work with policies that we're compiling -- if they don't know ref
|
||||
// heads, they wouldn't be able to parse them.
|
||||
const FeatureRefHeadStringPrefixes = "rule_head_ref_string_prefixes"
|
||||
const FeatureRefHeads = "rule_head_refs"
|
||||
const FeatureRegoV1Import = "rego_v1_import"
|
||||
|
||||
// Capabilities defines a structure containing data that describes the capabilities
|
||||
// or features supported by a particular version of OPA.
|
||||
type Capabilities struct {
|
||||
Builtins []*Builtin `json:"builtins"`
|
||||
FutureKeywords []string `json:"future_keywords"`
|
||||
WasmABIVersions []WasmABIVersion `json:"wasm_abi_versions"`
|
||||
Builtins []*Builtin `json:"builtins,omitempty"`
|
||||
FutureKeywords []string `json:"future_keywords,omitempty"`
|
||||
WasmABIVersions []WasmABIVersion `json:"wasm_abi_versions,omitempty"`
|
||||
|
||||
// Features is a bit of a mixed bag for checking that an older version of OPA
|
||||
// is able to do what needs to be done.
|
||||
// TODO(sr): find better words ^^
|
||||
Features []string `json:"features"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
|
||||
// allow_net is an array of hostnames or IP addresses, that an OPA instance is
|
||||
// allowed to connect to.
|
||||
@@ -73,6 +104,8 @@ func CapabilitiesForThisVersion() *Capabilities {
|
||||
|
||||
f.Features = []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1Import,
|
||||
}
|
||||
|
||||
return f
|
||||
@@ -129,3 +162,71 @@ func LoadCapabilitiesVersions() ([]string, error) {
|
||||
}
|
||||
return capabilitiesVersions, nil
|
||||
}
|
||||
|
||||
// MinimumCompatibleVersion returns the minimum compatible OPA version based on
|
||||
// the built-ins, features, and keywords in c.
|
||||
func (c *Capabilities) MinimumCompatibleVersion() (string, bool) {
|
||||
|
||||
var maxVersion semver.Version
|
||||
|
||||
// this is the oldest OPA release that includes capabilities
|
||||
if err := maxVersion.Set("0.17.0"); err != nil {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
for _, bi := range c.Builtins {
|
||||
v, ok := minVersionIndex.Builtins[bi.Name]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if v.Compare(maxVersion) > 0 {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
for _, kw := range c.FutureKeywords {
|
||||
v, ok := minVersionIndex.Keywords[kw]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if v.Compare(maxVersion) > 0 {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
for _, feat := range c.Features {
|
||||
v, ok := minVersionIndex.Features[feat]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if v.Compare(maxVersion) > 0 {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
return maxVersion.String(), true
|
||||
}
|
||||
|
||||
func (c *Capabilities) ContainsFeature(feature string) bool {
|
||||
for _, f := range c.Features {
|
||||
if f == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// addBuiltinSorted inserts a built-in into c in sorted order. An existing built-in with the same name
|
||||
// will be overwritten.
|
||||
func (c *Capabilities) addBuiltinSorted(bi *Builtin) {
|
||||
i := sort.Search(len(c.Builtins), func(x int) bool {
|
||||
return c.Builtins[x].Name >= bi.Name
|
||||
})
|
||||
if i < len(c.Builtins) && bi.Name == c.Builtins[i].Name {
|
||||
c.Builtins[i] = bi
|
||||
return
|
||||
}
|
||||
c.Builtins = append(c.Builtins, nil)
|
||||
copy(c.Builtins[i+1:], c.Builtins[i:])
|
||||
c.Builtins[i] = bi
|
||||
}
|
||||
|
||||
+79
-38
@@ -24,6 +24,8 @@ type exprChecker func(*TypeEnv, *Expr) *Error
|
||||
// accumulated on the typeChecker so that a single run can report multiple
|
||||
// issues.
|
||||
type typeChecker struct {
|
||||
builtins map[string]*Builtin
|
||||
required *Capabilities
|
||||
errs Errors
|
||||
exprCheckers map[string]exprChecker
|
||||
varRewriter varRewriter
|
||||
@@ -60,6 +62,16 @@ func (tc *typeChecker) copy() *typeChecker {
|
||||
WithInputType(tc.input)
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithRequiredCapabilities(c *Capabilities) *typeChecker {
|
||||
tc.required = c
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithBuiltins(builtins map[string]*Builtin) *typeChecker {
|
||||
tc.builtins = builtins
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithSchemaSet(ss *SchemaSet) *typeChecker {
|
||||
tc.ss = ss
|
||||
return tc
|
||||
@@ -177,27 +189,26 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
|
||||
|
||||
env = env.wrap()
|
||||
|
||||
if schemaAnnots := getRuleAnnotation(as, rule); schemaAnnots != nil {
|
||||
for _, schemaAnnot := range schemaAnnots {
|
||||
ref, refType, err := processAnnotation(tc.ss, schemaAnnot, rule, tc.allowNet)
|
||||
schemaAnnots := getRuleAnnotation(as, rule)
|
||||
for _, schemaAnnot := range schemaAnnots {
|
||||
ref, refType, err := processAnnotation(tc.ss, schemaAnnot, rule, tc.allowNet)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
if ref == nil && refType == nil {
|
||||
continue
|
||||
}
|
||||
prefixRef, t := getPrefix(env, ref)
|
||||
if t == nil || len(prefixRef) == len(ref) {
|
||||
env.tree.Put(ref, refType)
|
||||
} else {
|
||||
newType, err := override(ref[len(prefixRef):], t, refType, rule)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
if ref == nil && refType == nil {
|
||||
continue
|
||||
}
|
||||
prefixRef, t := getPrefix(env, ref)
|
||||
if t == nil || len(prefixRef) == len(ref) {
|
||||
env.tree.Put(ref, refType)
|
||||
} else {
|
||||
newType, err := override(ref[len(prefixRef):], t, refType, rule)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
env.tree.Put(prefixRef, newType)
|
||||
}
|
||||
env.tree.Put(prefixRef, newType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,47 +243,66 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
|
||||
|
||||
f := types.NewFunction(args, cpy.Get(rule.Head.Value))
|
||||
|
||||
// Union with existing.
|
||||
exist := env.tree.Get(path)
|
||||
tpe = types.Or(exist, f)
|
||||
|
||||
tpe = f
|
||||
} else {
|
||||
switch rule.Head.RuleKind() {
|
||||
case SingleValue:
|
||||
typeV := cpy.Get(rule.Head.Value)
|
||||
if last := path[len(path)-1]; !last.IsGround() {
|
||||
|
||||
// e.g. store object[string: whatever] at data.p.q.r, not data.p.q.r[x]
|
||||
if !path.IsGround() {
|
||||
// e.g. store object[string: whatever] at data.p.q.r, not data.p.q.r[x] or data.p.q.r[x].y[z]
|
||||
objPath := path.DynamicSuffix()
|
||||
path = path.GroundPrefix()
|
||||
|
||||
typeK := cpy.Get(last)
|
||||
if typeK != nil && typeV != nil {
|
||||
exist := env.tree.Get(path)
|
||||
typeV = types.Or(types.Values(exist), typeV)
|
||||
typeK = types.Or(types.Keys(exist), typeK)
|
||||
tpe = types.NewObject(nil, types.NewDynamicProperty(typeK, typeV))
|
||||
var err error
|
||||
tpe, err = nestedObject(cpy, objPath, typeV)
|
||||
if err != nil {
|
||||
tc.err([]*Error{NewError(TypeErr, rule.Head.Location, err.Error())})
|
||||
tpe = nil
|
||||
}
|
||||
} else {
|
||||
if typeV != nil {
|
||||
exist := env.tree.Get(path)
|
||||
tpe = types.Or(typeV, exist)
|
||||
tpe = typeV
|
||||
}
|
||||
}
|
||||
case MultiValue:
|
||||
typeK := cpy.Get(rule.Head.Key)
|
||||
if typeK != nil {
|
||||
exist := env.tree.Get(path)
|
||||
typeK = types.Or(types.Keys(exist), typeK)
|
||||
tpe = types.NewSet(typeK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tpe != nil {
|
||||
env.tree.Put(path, tpe)
|
||||
env.tree.Insert(path, tpe, env)
|
||||
}
|
||||
}
|
||||
|
||||
// nestedObject creates a nested structure of object types, where each term on path corresponds to a level in the
|
||||
// nesting. Each term in the path only contributes to the dynamic portion of its corresponding object.
|
||||
func nestedObject(env *TypeEnv, path Ref, tpe types.Type) (types.Type, error) {
|
||||
if len(path) == 0 {
|
||||
return tpe, nil
|
||||
}
|
||||
|
||||
k := path[0]
|
||||
typeV, err := nestedObject(env, path[1:], tpe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typeV == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var dynamicProperty *types.DynamicProperty
|
||||
typeK := env.Get(k)
|
||||
if typeK == nil {
|
||||
return nil, nil
|
||||
}
|
||||
dynamicProperty = types.NewDynamicProperty(typeK, typeV)
|
||||
|
||||
return types.NewObject(nil, dynamicProperty), nil
|
||||
}
|
||||
|
||||
func (tc *typeChecker) checkExpr(env *TypeEnv, expr *Expr) *Error {
|
||||
if err := tc.checkExprWith(env, expr, 0); err != nil {
|
||||
return err
|
||||
@@ -281,7 +311,18 @@ func (tc *typeChecker) checkExpr(env *TypeEnv, expr *Expr) *Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
checker := tc.exprCheckers[expr.Operator().String()]
|
||||
operator := expr.Operator().String()
|
||||
|
||||
// If the type checker wasn't provided with a required capabilities
|
||||
// structure then just skip. In some cases, type checking might be run
|
||||
// without the need to record what builtins are required.
|
||||
if tc.required != nil {
|
||||
if bi, ok := tc.builtins[operator]; ok {
|
||||
tc.required.addBuiltinSorted(bi)
|
||||
}
|
||||
}
|
||||
|
||||
checker := tc.exprCheckers[operator]
|
||||
if checker != nil {
|
||||
return checker(env, expr)
|
||||
}
|
||||
@@ -389,7 +430,7 @@ func (tc *typeChecker) checkExprWith(env *TypeEnv, expr *Expr, i int) *Error {
|
||||
switch v := valueType.(type) {
|
||||
case *types.Function: // ...by function
|
||||
if !unifies(targetType, valueType) {
|
||||
return newArgError(expr.With[i].Loc(), target.Value.(Ref), "arity mismatch", v.Args(), t.NamedFuncArgs())
|
||||
return newArgError(expr.With[i].Loc(), target.Value.(Ref), "arity mismatch", v.FuncArgs().Args, t.NamedFuncArgs())
|
||||
}
|
||||
default: // ... by value, nothing to check
|
||||
}
|
||||
@@ -1200,7 +1241,7 @@ func getRuleAnnotation(as *AnnotationSet, rule *Rule) (result []*SchemaAnnotatio
|
||||
result = append(result, x.Schemas...)
|
||||
}
|
||||
|
||||
if x := as.GetDocumentScope(rule.Path()); x != nil {
|
||||
if x := as.GetDocumentScope(rule.Ref().GroundPrefix()); x != nil {
|
||||
result = append(result, x.Schemas...)
|
||||
}
|
||||
|
||||
|
||||
+415
-199
File diff suppressed because it is too large
Load Diff
+187
@@ -6,6 +6,7 @@ package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/types"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
@@ -171,6 +172,11 @@ func (env *TypeEnv) getRefRec(node *typeTreeNode, ref, tail Ref) types.Type {
|
||||
}
|
||||
|
||||
if node.Leaf() {
|
||||
if node.children.Len() > 0 {
|
||||
if child := node.Child(tail[0].Value); child != nil {
|
||||
return env.getRefRec(child, ref, tail[1:])
|
||||
}
|
||||
}
|
||||
return selectRef(node.Value(), tail)
|
||||
}
|
||||
|
||||
@@ -304,6 +310,187 @@ func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
|
||||
curr.value = tpe
|
||||
}
|
||||
|
||||
// Insert inserts tpe at path in the tree, but also merges the value into any types.Object present along that path.
|
||||
// If a types.Object is inserted, any leafs already present further down the tree are merged into the inserted object.
|
||||
// path must be ground.
|
||||
func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
|
||||
curr := n
|
||||
for i, term := range path {
|
||||
c, ok := curr.children.Get(term.Value)
|
||||
|
||||
var child *typeTreeNode
|
||||
if !ok {
|
||||
child = newTypeTree()
|
||||
child.key = term.Value
|
||||
curr.children.Put(child.key, child)
|
||||
} else {
|
||||
child = c.(*typeTreeNode)
|
||||
|
||||
if child.value != nil && i+1 < len(path) {
|
||||
// If child has an object value, merge the new value into it.
|
||||
if o, ok := child.value.(*types.Object); ok {
|
||||
var err error
|
||||
child.value, err = insertIntoObject(o, path[i+1:], tpe, env)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curr = child
|
||||
}
|
||||
|
||||
curr.value = mergeTypes(curr.value, tpe)
|
||||
|
||||
if _, ok := tpe.(*types.Object); ok && curr.children.Len() > 0 {
|
||||
// merge all leafs into the inserted object
|
||||
leafs := curr.Leafs()
|
||||
for p, t := range leafs {
|
||||
var err error
|
||||
curr.value, err = insertIntoObject(curr.value.(*types.Object), *p, t, env)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeTypes merges the types of 'a' and 'b'. If both are sets, their 'of' types are joined with an types.Or.
|
||||
// If both are objects, the key types of their dynamic properties are joined with types.Or:s, and their value types
|
||||
// are recursively merged (using mergeTypes).
|
||||
// If 'a' and 'b' are both objects, and at least one of them have static properties, they are joined
|
||||
// with an types.Or, instead of being merged.
|
||||
// If 'a' is an Any containing an Object, and 'b' is an Object (or vice versa); AND both objects have no
|
||||
// static properties, they are merged.
|
||||
// If 'a' and 'b' are different types, they are joined with an types.Or.
|
||||
func mergeTypes(a, b types.Type) types.Type {
|
||||
if a == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
if b == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
switch a := a.(type) {
|
||||
case *types.Object:
|
||||
if bObj, ok := b.(*types.Object); ok && len(a.StaticProperties()) == 0 && len(bObj.StaticProperties()) == 0 {
|
||||
if len(a.StaticProperties()) > 0 || len(bObj.StaticProperties()) > 0 {
|
||||
return types.Or(a, bObj)
|
||||
}
|
||||
|
||||
aDynProps := a.DynamicProperties()
|
||||
bDynProps := bObj.DynamicProperties()
|
||||
dynProps := types.NewDynamicProperty(
|
||||
types.Or(aDynProps.Key, bDynProps.Key),
|
||||
mergeTypes(aDynProps.Value, bDynProps.Value))
|
||||
return types.NewObject(nil, dynProps)
|
||||
} else if bAny, ok := b.(types.Any); ok && len(a.StaticProperties()) == 0 {
|
||||
// If a is an object type with no static components ...
|
||||
for _, t := range bAny {
|
||||
if tObj, ok := t.(*types.Object); ok && len(tObj.StaticProperties()) == 0 {
|
||||
// ... and b is a types.Any containing an object with no static components, we merge them.
|
||||
aDynProps := a.DynamicProperties()
|
||||
tDynProps := tObj.DynamicProperties()
|
||||
tDynProps.Key = types.Or(tDynProps.Key, aDynProps.Key)
|
||||
tDynProps.Value = types.Or(tDynProps.Value, aDynProps.Value)
|
||||
return bAny
|
||||
}
|
||||
}
|
||||
}
|
||||
case *types.Set:
|
||||
if bSet, ok := b.(*types.Set); ok {
|
||||
return types.NewSet(types.Or(a.Of(), bSet.Of()))
|
||||
}
|
||||
case types.Any:
|
||||
if _, ok := b.(types.Any); !ok {
|
||||
return mergeTypes(b, a)
|
||||
}
|
||||
}
|
||||
|
||||
return types.Or(a, b)
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) String() string {
|
||||
b := strings.Builder{}
|
||||
|
||||
if k := n.key; k != nil {
|
||||
b.WriteString(k.String())
|
||||
} else {
|
||||
b.WriteString("-")
|
||||
}
|
||||
|
||||
if v := n.value; v != nil {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(v.String())
|
||||
}
|
||||
|
||||
n.children.Iter(func(_, v util.T) bool {
|
||||
if child, ok := v.(*typeTreeNode); ok {
|
||||
b.WriteString("\n\t+ ")
|
||||
s := child.String()
|
||||
s = strings.ReplaceAll(s, "\n", "\n\t")
|
||||
b.WriteString(s)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func insertIntoObject(o *types.Object, path Ref, tpe types.Type, env *TypeEnv) (*types.Object, error) {
|
||||
if len(path) == 0 {
|
||||
return o, nil
|
||||
}
|
||||
|
||||
key := env.Get(path[0].Value)
|
||||
|
||||
if len(path) == 1 {
|
||||
var dynamicProps *types.DynamicProperty
|
||||
if dp := o.DynamicProperties(); dp != nil {
|
||||
dynamicProps = types.NewDynamicProperty(types.Or(o.DynamicProperties().Key, key), types.Or(o.DynamicProperties().Value, tpe))
|
||||
} else {
|
||||
dynamicProps = types.NewDynamicProperty(key, tpe)
|
||||
}
|
||||
return types.NewObject(o.StaticProperties(), dynamicProps), nil
|
||||
}
|
||||
|
||||
child, err := insertIntoObject(types.NewObject(nil, nil), path[1:], tpe, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dynamicProps *types.DynamicProperty
|
||||
if dp := o.DynamicProperties(); dp != nil {
|
||||
dynamicProps = types.NewDynamicProperty(types.Or(o.DynamicProperties().Key, key), types.Or(o.DynamicProperties().Value, child))
|
||||
} else {
|
||||
dynamicProps = types.NewDynamicProperty(key, child)
|
||||
}
|
||||
return types.NewObject(o.StaticProperties(), dynamicProps), nil
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) Leafs() map[*Ref]types.Type {
|
||||
leafs := map[*Ref]types.Type{}
|
||||
n.children.Iter(func(k, v util.T) bool {
|
||||
collectLeafs(v.(*typeTreeNode), nil, leafs)
|
||||
return false
|
||||
})
|
||||
return leafs
|
||||
}
|
||||
|
||||
func collectLeafs(n *typeTreeNode, path Ref, leafs map[*Ref]types.Type) {
|
||||
nPath := append(path, NewTerm(n.key))
|
||||
if n.Leaf() {
|
||||
leafs[&nPath] = n.Value()
|
||||
return
|
||||
}
|
||||
n.children.Iter(func(k, v util.T) bool {
|
||||
collectLeafs(v.(*typeTreeNode), nPath, leafs)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) Value() types.Type {
|
||||
return n.value
|
||||
}
|
||||
|
||||
+11
-8
@@ -173,6 +173,7 @@ func (i *baseDocEqIndex) AllRules(resolver ValueResolver) (*IndexResult, error)
|
||||
|
||||
result := NewIndexResult(i.kind)
|
||||
result.Default = i.defaultRule
|
||||
result.OnlyGroundRefs = i.onlyGroundRefs
|
||||
result.Rules = make([]*Rule, 0, len(tr.ordering))
|
||||
|
||||
for _, pos := range tr.ordering {
|
||||
@@ -482,8 +483,10 @@ func (node *trieNode) String() string {
|
||||
if len(node.mappers) > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d mapper(s)", len(node.mappers)))
|
||||
}
|
||||
if l := node.values.Len(); l > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d value(s)", l))
|
||||
if node.values != nil {
|
||||
if l := node.values.Len(); l > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d value(s)", l))
|
||||
}
|
||||
}
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
@@ -697,12 +700,6 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
|
||||
return node.Traverse(resolver, tr)
|
||||
}
|
||||
|
||||
head := arr.Elem(0).Value
|
||||
|
||||
if !IsScalar(head) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.any != nil {
|
||||
err := node.any.traverseArray(resolver, tr, arr.Slice(1, -1))
|
||||
if err != nil {
|
||||
@@ -710,6 +707,12 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
|
||||
}
|
||||
}
|
||||
|
||||
head := arr.Elem(0).Value
|
||||
|
||||
if !IsScalar(head) {
|
||||
return nil
|
||||
}
|
||||
|
||||
child, ok := node.scalars.Get(head)
|
||||
if !ok {
|
||||
return nil
|
||||
|
||||
+26
-8
@@ -18,14 +18,15 @@ const bom = 0xFEFF
|
||||
// Scanner is used to tokenize an input stream of
|
||||
// Rego source code.
|
||||
type Scanner struct {
|
||||
offset int
|
||||
row int
|
||||
col int
|
||||
bs []byte
|
||||
curr rune
|
||||
width int
|
||||
errors []Error
|
||||
keywords map[string]tokens.Token
|
||||
offset int
|
||||
row int
|
||||
col int
|
||||
bs []byte
|
||||
curr rune
|
||||
width int
|
||||
errors []Error
|
||||
keywords map[string]tokens.Token
|
||||
regoV1Compatible bool
|
||||
}
|
||||
|
||||
// Error represents a scanner error.
|
||||
@@ -102,6 +103,23 @@ func (s *Scanner) AddKeyword(kw string, tok tokens.Token) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) HasKeyword(keywords map[string]tokens.Token) bool {
|
||||
for kw := range s.keywords {
|
||||
if _, ok := keywords[kw]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scanner) SetRegoV1Compatible() {
|
||||
s.regoV1Compatible = true
|
||||
}
|
||||
|
||||
func (s *Scanner) RegoV1Compatible() bool {
|
||||
return s.regoV1Compatible
|
||||
}
|
||||
|
||||
// WithKeywords returns a new copy of the Scanner struct `s`, with the set
|
||||
// of known keywords being that of `s` with `kws` added.
|
||||
func (s *Scanner) WithKeywords(kws map[string]tokens.Token) *Scanner {
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ var strings = [...]string{
|
||||
EOF: "eof",
|
||||
Whitespace: "whitespace",
|
||||
Comment: "comment",
|
||||
Ident: "ident",
|
||||
Ident: "identifier",
|
||||
Package: "package",
|
||||
Import: "import",
|
||||
As: "as",
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package json
|
||||
|
||||
// Options defines the options for JSON operations,
|
||||
// currently only marshaling can be configured
|
||||
type Options struct {
|
||||
MarshalOptions MarshalOptions
|
||||
}
|
||||
|
||||
// MarshalOptions defines the options for JSON marshaling,
|
||||
// currently only toggling the marshaling of location information is supported
|
||||
type MarshalOptions struct {
|
||||
// IncludeLocation toggles the marshaling of location information
|
||||
IncludeLocation NodeToggle
|
||||
// IncludeLocationText additionally/optionally includes the text of the location
|
||||
IncludeLocationText bool
|
||||
// ExcludeLocationFile additionally/optionally excludes the file of the location
|
||||
// Note that this is inverted (i.e. not "include" as the default needs to remain false)
|
||||
ExcludeLocationFile bool
|
||||
}
|
||||
|
||||
// NodeToggle is a generic struct to allow the toggling of
|
||||
// settings for different ast node types
|
||||
type NodeToggle struct {
|
||||
Term bool
|
||||
Package bool
|
||||
Comment bool
|
||||
Import bool
|
||||
Rule bool
|
||||
Head bool
|
||||
Expr bool
|
||||
SomeDecl bool
|
||||
Every bool
|
||||
With bool
|
||||
Annotations bool
|
||||
AnnotationsRef bool
|
||||
}
|
||||
+43
@@ -3,8 +3,11 @@ package location
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// Location records a position in source code
|
||||
@@ -14,6 +17,9 @@ type Location struct {
|
||||
Row int `json:"row"` // The line in the source.
|
||||
Col int `json:"col"` // The column in the row.
|
||||
Offset int `json:"-"` // The byte offset for the location in the source.
|
||||
|
||||
// JSONOptions specifies options for marshaling and unmarshalling of locations
|
||||
JSONOptions astJSON.Options
|
||||
}
|
||||
|
||||
// NewLocation returns a new Location object.
|
||||
@@ -87,3 +93,40 @@ 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
|
||||
if loc.JSONOptions.MarshalOptions.ExcludeLocationFile {
|
||||
data := struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
}
|
||||
|
||||
if loc.JSONOptions.MarshalOptions.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 loc.JSONOptions.MarshalOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// customJSON is an interface that can be implemented by AST nodes that
|
||||
// allows the parser to set options for JSON operations on that node.
|
||||
type customJSON interface {
|
||||
setJSONOptions(JSONOptions)
|
||||
setJSONOptions(astJSON.Options)
|
||||
}
|
||||
|
||||
+133
-72
@@ -21,9 +21,12 @@ import (
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/scanner"
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/ast/location"
|
||||
)
|
||||
|
||||
var RegoV1CompatibleRef = Ref{VarTerm("rego"), StringTerm("v1")}
|
||||
|
||||
// Note: This state is kept isolated from the parser so that we
|
||||
// can do efficient shallow copies of these values when doing a
|
||||
// save() and restore().
|
||||
@@ -84,7 +87,7 @@ func (c parsedTermCache) String() string {
|
||||
s.WriteRune('{')
|
||||
var e *parsedTermCacheItem
|
||||
for e = c.m; e != nil; e = e.next {
|
||||
fmt.Fprintf(&s, "%v", e)
|
||||
s.WriteString(fmt.Sprintf("%v", e))
|
||||
}
|
||||
s.WriteRune('}')
|
||||
return s.String()
|
||||
@@ -101,37 +104,9 @@ type ParserOptions struct {
|
||||
AllFutureKeywords bool
|
||||
FutureKeywords []string
|
||||
SkipRules bool
|
||||
JSONOptions *JSONOptions
|
||||
JSONOptions *astJSON.Options
|
||||
unreleasedKeywords bool // TODO(sr): cleanup
|
||||
}
|
||||
|
||||
// JSONOptions defines the options for JSON operations,
|
||||
// currently only marshaling can be configured
|
||||
type JSONOptions struct {
|
||||
MarshalOptions JSONMarshalOptions
|
||||
}
|
||||
|
||||
// JSONMarshalOptions defines the options for JSON marshaling,
|
||||
// currently only toggling the marshaling of location information is supported
|
||||
type JSONMarshalOptions struct {
|
||||
IncludeLocation NodeToggle
|
||||
}
|
||||
|
||||
// NodeToggle is a generic struct to allow the toggling of
|
||||
// settings for different ast node types
|
||||
type NodeToggle struct {
|
||||
Term bool
|
||||
Package bool
|
||||
Comment bool
|
||||
Import bool
|
||||
Rule bool
|
||||
Head bool
|
||||
Expr bool
|
||||
SomeDecl bool
|
||||
Every bool
|
||||
With bool
|
||||
Annotations bool
|
||||
AnnotationsRef bool
|
||||
RegoV1Compatible bool
|
||||
}
|
||||
|
||||
// NewParser creates and initializes a Parser.
|
||||
@@ -207,9 +182,9 @@ func (p *Parser) WithSkipRules(skip bool) *Parser {
|
||||
return p
|
||||
}
|
||||
|
||||
// WithJSONOptions sets the JSONOptions which will be set on nodes to configure
|
||||
// WithJSONOptions sets the Options which will be set on nodes to configure
|
||||
// their JSON marshaling behavior.
|
||||
func (p *Parser) WithJSONOptions(jsonOptions *JSONOptions) *Parser {
|
||||
func (p *Parser) WithJSONOptions(jsonOptions *astJSON.Options) *Parser {
|
||||
p.po.JSONOptions = jsonOptions
|
||||
return p
|
||||
}
|
||||
@@ -356,9 +331,14 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
|
||||
s = p.save()
|
||||
|
||||
if imp := p.parseImport(); imp != nil {
|
||||
if RegoRootDocument.Equal(imp.Path.Value.(Ref)[0]) {
|
||||
p.regoV1Import(imp)
|
||||
}
|
||||
|
||||
if FutureRootDocument.Equal(imp.Path.Value.(Ref)[0]) {
|
||||
p.futureImport(imp, allowedFutureKeywords)
|
||||
}
|
||||
|
||||
stmts = append(stmts, imp)
|
||||
continue
|
||||
} else if len(p.s.errors) > 0 {
|
||||
@@ -561,9 +541,9 @@ func (p *Parser) parseImport() *Import {
|
||||
|
||||
path := imp.Path.Value.(Ref)
|
||||
|
||||
if !RootDocumentNames.Contains(path[0]) && !FutureRootDocument.Equal(path[0]) {
|
||||
if !RootDocumentNames.Contains(path[0]) && !FutureRootDocument.Equal(path[0]) && !RegoRootDocument.Equal(path[0]) {
|
||||
p.errorf(imp.Path.Location, "unexpected import path, must begin with one of: %v, got: %v",
|
||||
RootDocumentNames.Union(NewSet(FutureRootDocument)),
|
||||
RootDocumentNames.Union(NewSet(FutureRootDocument, RegoRootDocument)),
|
||||
path[0])
|
||||
return nil
|
||||
}
|
||||
@@ -609,26 +589,32 @@ func (p *Parser) parseRules() []*Rule {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usesContains {
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.Contains)
|
||||
}
|
||||
|
||||
if rule.Default {
|
||||
if !p.validateDefaultRuleValue(&rule) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(rule.Head.Args) > 0 {
|
||||
if !p.validateDefaultRuleArgs(&rule) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location))
|
||||
return []*Rule{&rule}
|
||||
}
|
||||
|
||||
if usesContains && !rule.Head.Reference.IsGround() {
|
||||
p.error(p.s.Loc(), "multi-value rules need ground refs")
|
||||
return nil
|
||||
}
|
||||
|
||||
// back-compat with `p[x] { ... }``
|
||||
hasIf := p.s.tok == tokens.If
|
||||
|
||||
// p[x] if ... becomes a single-value rule p[x]
|
||||
if hasIf && !usesContains && len(rule.Head.Ref()) == 2 {
|
||||
if rule.Head.Value == nil {
|
||||
rule.Head.generatedValue = true
|
||||
rule.Head.Value = BooleanTerm(true).SetLocation(rule.Head.Location)
|
||||
} else {
|
||||
// p[x] = y if becomes a single-value rule p[x] with value y, but needs name for compat
|
||||
@@ -657,6 +643,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
|
||||
switch {
|
||||
case hasIf:
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.If)
|
||||
p.scan()
|
||||
s := p.save()
|
||||
if expr := p.parseLiteral(); expr != nil {
|
||||
@@ -688,6 +675,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
|
||||
case usesContains:
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location))
|
||||
rule.generatedBody = true
|
||||
return []*Rule{&rule}
|
||||
|
||||
default:
|
||||
@@ -695,7 +683,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.Else {
|
||||
if r := rule.Head.Ref(); len(r) > 1 && !r[len(r)-1].Value.IsGround() {
|
||||
if r := rule.Head.Ref(); len(r) > 1 && !r.IsGround() {
|
||||
p.error(p.s.Loc(), "else keyword cannot be used on rules with variables in head")
|
||||
return nil
|
||||
}
|
||||
@@ -737,6 +725,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
// rule's head AST but have their location
|
||||
// set to the rule body.
|
||||
next.Head = rule.Head.Copy()
|
||||
next.Head.keywords = rule.Head.keywords
|
||||
for i := range next.Head.Args {
|
||||
if v, ok := next.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
|
||||
next.Head.Args[i].Value = Var(p.genwildcard())
|
||||
@@ -756,6 +745,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
rule.SetLoc(p.s.Loc())
|
||||
|
||||
rule.Head = head.Copy()
|
||||
rule.Head.generatedValue = false
|
||||
for i := range rule.Head.Args {
|
||||
if v, ok := rule.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
|
||||
rule.Head.Args[i].Value = Var(p.genwildcard())
|
||||
@@ -771,6 +761,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
|
||||
switch p.s.tok {
|
||||
case tokens.LBrace, tokens.If: // no value, but a body follows directly
|
||||
rule.Head.generatedValue = true
|
||||
rule.Head.Value = BooleanTerm(true)
|
||||
case tokens.Assign, tokens.Unify:
|
||||
rule.Head.Assign = tokens.Assign == p.s.tok
|
||||
@@ -786,42 +777,37 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
}
|
||||
|
||||
hasIf := p.s.tok == tokens.If
|
||||
hasLBrace := p.s.tok == tokens.LBrace
|
||||
|
||||
if hasIf {
|
||||
p.scan()
|
||||
s := p.save()
|
||||
if expr := p.parseLiteral(); expr != nil {
|
||||
// NOTE(sr): set literals are never false or undefined, so parsing this as
|
||||
// p if false else if { true }
|
||||
// ^^^^^^^^ set of one element, `true`
|
||||
// isn't valid.
|
||||
isSetLiteral := false
|
||||
if t, ok := expr.Terms.(*Term); ok {
|
||||
_, isSetLiteral = t.Value.(Set)
|
||||
}
|
||||
// expr.Term is []*Term or Every
|
||||
if !isSetLiteral {
|
||||
rule.Body.Append(expr)
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
return &rule
|
||||
}
|
||||
}
|
||||
p.restore(s)
|
||||
}
|
||||
|
||||
if p.s.tok != tokens.LBrace {
|
||||
if !hasIf && !hasLBrace {
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true)))
|
||||
rule.generatedBody = true
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
return &rule
|
||||
}
|
||||
|
||||
p.scan()
|
||||
|
||||
if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil {
|
||||
return nil
|
||||
if hasIf {
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.If)
|
||||
p.scan()
|
||||
}
|
||||
|
||||
p.scan()
|
||||
if p.s.tok == tokens.LBrace {
|
||||
p.scan()
|
||||
if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil {
|
||||
return nil
|
||||
}
|
||||
p.scan()
|
||||
} else if p.s.tok != tokens.EOF {
|
||||
expr := p.parseLiteral()
|
||||
if expr == nil {
|
||||
return nil
|
||||
}
|
||||
rule.Body.Append(expr)
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
} else {
|
||||
p.illegal("rule body expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.Else {
|
||||
if rule.Else = p.parseElse(head); rule.Else == nil {
|
||||
@@ -832,7 +818,6 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
}
|
||||
|
||||
func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
head := &Head{}
|
||||
loc := p.s.Loc()
|
||||
defer func() {
|
||||
@@ -855,7 +840,9 @@ func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
switch x := ref.Value.(type) {
|
||||
case Var:
|
||||
head = NewHead(x)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(x, ref.Location, p.po.JSONOptions)
|
||||
case Ref:
|
||||
head = RefHead(x)
|
||||
case Call:
|
||||
@@ -922,6 +909,7 @@ func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
if head.Value == nil && head.Key == nil {
|
||||
if len(head.Ref()) != 2 || len(head.Args) > 0 {
|
||||
head.generatedValue = true
|
||||
head.Value = BooleanTerm(true).SetLocation(head.Location)
|
||||
}
|
||||
}
|
||||
@@ -2001,7 +1989,7 @@ func (p *Parser) error(loc *location.Location, reason string) {
|
||||
|
||||
func (p *Parser) errorf(loc *location.Location, f string, a ...interface{}) {
|
||||
msg := strings.Builder{}
|
||||
fmt.Fprintf(&msg, f, a...)
|
||||
msg.WriteString(fmt.Sprintf(f, a...))
|
||||
|
||||
switch len(p.s.hints) {
|
||||
case 0: // nothing to do
|
||||
@@ -2176,6 +2164,38 @@ func (p *Parser) validateDefaultRuleValue(rule *Rule) bool {
|
||||
return valid
|
||||
}
|
||||
|
||||
func (p *Parser) validateDefaultRuleArgs(rule *Rule) bool {
|
||||
|
||||
valid := true
|
||||
vars := NewVarSet()
|
||||
|
||||
vis := NewGenericVisitor(func(x interface{}) bool {
|
||||
switch x := x.(type) {
|
||||
case Var:
|
||||
if vars.Contains(x) {
|
||||
p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot be repeated %v)", x))
|
||||
valid = false
|
||||
return true
|
||||
}
|
||||
vars.Add(x)
|
||||
|
||||
case *Term:
|
||||
switch v := x.Value.(type) {
|
||||
case Var: // do nothing
|
||||
default:
|
||||
p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot contain %v)", TypeName(v)))
|
||||
valid = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
vis.Walk(rule.Head.Args)
|
||||
return valid
|
||||
}
|
||||
|
||||
// We explicitly use yaml unmarshalling, to accommodate for the '_' in 'related_resources',
|
||||
// which isn't handled properly by json for some reason.
|
||||
type rawAnnotation struct {
|
||||
@@ -2508,6 +2528,11 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
return
|
||||
}
|
||||
|
||||
if p.s.s.RegoV1Compatible() {
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
kwds := make([]string, 0, len(allowedFutureKeywords))
|
||||
for k := range allowedFutureKeywords {
|
||||
kwds = append(kwds, k)
|
||||
@@ -2535,3 +2560,39 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
p.s.s.AddKeyword(kw, allowedFutureKeywords[kw])
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) regoV1Import(imp *Import) {
|
||||
if !p.po.Capabilities.ContainsFeature(FeatureRegoV1Import) {
|
||||
p.errorf(imp.Path.Location, "invalid import, `%s` is not supported by current capabilities", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
path := imp.Path.Value.(Ref)
|
||||
|
||||
if len(path) == 1 || !path[1].Equal(RegoV1CompatibleRef[1]) || len(path) > 2 {
|
||||
p.errorf(imp.Path.Location, "invalid import, must be `%s`", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
if imp.Alias != "" {
|
||||
p.errorf(imp.Path.Location, "`rego` imports cannot be aliased")
|
||||
return
|
||||
}
|
||||
|
||||
// import all future keywords with the rego.v1 import
|
||||
kwds := make([]string, 0, len(futureKeywords))
|
||||
for k := range futureKeywords {
|
||||
kwds = append(kwds, k)
|
||||
}
|
||||
|
||||
if p.s.s.HasKeyword(futureKeywords) && !p.s.s.RegoV1Compatible() {
|
||||
// We have imported future keywords, but they didn't come from another `rego.v1` import.
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
p.s.s.SetRegoV1Compatible()
|
||||
for _, kw := range kwds {
|
||||
p.s.s.AddKeyword(kw, futureKeywords[kw])
|
||||
}
|
||||
}
|
||||
|
||||
+112
-34
@@ -16,6 +16,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// MustParseBody returns a parsed body.
|
||||
@@ -244,7 +247,9 @@ func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, erro
|
||||
var head *Head
|
||||
|
||||
if v, ok := lhs.Value.(Var); ok {
|
||||
head = NewHead(v)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(v, lhs.Location, &lhs.jsonOptions)
|
||||
} else if r, ok := lhs.Value.(Ref); ok { // groundness ?
|
||||
if _, ok := r[0].Value.(Var); !ok {
|
||||
return nil, fmt.Errorf("invalid rule head: %v", r)
|
||||
@@ -258,14 +263,17 @@ func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, erro
|
||||
}
|
||||
head.Value = rhs
|
||||
head.Location = lhs.Location
|
||||
head.setJSONOptions(lhs.jsonOptions)
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
return &Rule{
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: lhs.jsonOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -279,15 +287,20 @@ func ParseCompleteDocRuleWithDotsFromTerm(module *Module, term *Term) (*Rule, er
|
||||
return nil, fmt.Errorf("invalid rule head: %v", ref)
|
||||
}
|
||||
head := RefHead(ref, BooleanTerm(true).SetLocation(term.Location))
|
||||
head.generatedValue = true
|
||||
head.Location = term.Location
|
||||
head.jsonOptions = term.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location))
|
||||
setJSONOptions(body, &term.jsonOptions)
|
||||
|
||||
return &Rule{
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location),
|
||||
),
|
||||
Module: module,
|
||||
Body: body,
|
||||
Module: module,
|
||||
|
||||
jsonOptions: term.jsonOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -309,14 +322,17 @@ func ParsePartialObjectDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule,
|
||||
head.Key = ref[1]
|
||||
}
|
||||
head.Location = rhs.Location
|
||||
head.jsonOptions = rhs.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: rhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: rhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: rhs.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -340,18 +356,23 @@ func ParsePartialSetDocRuleFromTerm(module *Module, term *Term) (*Rule, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%vs cannot be used for rule head", TypeName(term.Value))
|
||||
}
|
||||
head = NewHead(v)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(v, ref[0].Location, &ref[0].jsonOptions)
|
||||
head.Key = ref[1]
|
||||
}
|
||||
head.Location = term.Location
|
||||
head.jsonOptions = term.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location))
|
||||
setJSONOptions(body, &term.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: term.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -377,12 +398,17 @@ func ParseRuleFromCallEqExpr(module *Module, lhs, rhs *Term) (*Rule, error) {
|
||||
head := RefHead(ref, rhs)
|
||||
head.Location = lhs.Location
|
||||
head.Args = Args(call[1:])
|
||||
head.jsonOptions = lhs.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location)),
|
||||
Module: module,
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: lhs.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -404,12 +430,17 @@ func ParseRuleFromCallExpr(module *Module, terms []*Term) (*Rule, error) {
|
||||
head := RefHead(ref, BooleanTerm(true).SetLocation(loc))
|
||||
head.Location = loc
|
||||
head.Args = terms[1:]
|
||||
head.jsonOptions = terms[0].jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(loc)).SetLocation(loc))
|
||||
setJSONOptions(body, &terms[0].jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: loc,
|
||||
Head: head,
|
||||
Module: module,
|
||||
Body: NewBody(NewExpr(BooleanTerm(true).SetLocation(loc)).SetLocation(loc)),
|
||||
Location: loc,
|
||||
Head: head,
|
||||
Module: module,
|
||||
Body: body,
|
||||
jsonOptions: terms[0].jsonOptions,
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
@@ -446,7 +477,7 @@ func ParseModuleWithOpts(filename, input string, popts ParserOptions) (*Module,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseModule(filename, stmts, comments)
|
||||
return parseModule(filename, stmts, comments, popts.RegoV1Compatible)
|
||||
}
|
||||
|
||||
// ParseBody returns exactly one body.
|
||||
@@ -606,7 +637,7 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
|
||||
return stmts, comments, nil
|
||||
}
|
||||
|
||||
func parseModule(filename string, stmts []Statement, comments []*Comment) (*Module, error) {
|
||||
func parseModule(filename string, stmts []Statement, comments []*Comment, regoV1Compatible bool) (*Module, error) {
|
||||
|
||||
if len(stmts) == 0 {
|
||||
return nil, NewError(ParseErr, &Location{File: filename}, "empty module")
|
||||
@@ -627,11 +658,15 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
|
||||
// The comments slice only holds comments that were not their own statements.
|
||||
mod.Comments = append(mod.Comments, comments...)
|
||||
mod.regoV1Compatible = regoV1Compatible
|
||||
|
||||
for i, stmt := range stmts[1:] {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Import:
|
||||
mod.Imports = append(mod.Imports, stmt)
|
||||
if Compare(stmt.Path.Value, RegoV1CompatibleRef) == 0 {
|
||||
mod.regoV1Compatible = true
|
||||
}
|
||||
case *Rule:
|
||||
setRuleModule(stmt, mod)
|
||||
mod.Rules = append(mod.Rules, stmt)
|
||||
@@ -641,6 +676,7 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
errs = append(errs, NewError(ParseErr, stmt[0].Location, err.Error()))
|
||||
continue
|
||||
}
|
||||
rule.generatedBody = true
|
||||
mod.Rules = append(mod.Rules, rule)
|
||||
|
||||
// NOTE(tsandall): the statement should now be interpreted as a
|
||||
@@ -658,6 +694,29 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
}
|
||||
}
|
||||
|
||||
if mod.regoV1Compatible {
|
||||
for _, rule := range mod.Rules {
|
||||
for r := rule; r != nil; r = r.Else {
|
||||
var t string
|
||||
if r.isFunction() {
|
||||
t = "function"
|
||||
} else {
|
||||
t = "rule"
|
||||
}
|
||||
|
||||
if r.generatedBody && r.Head.generatedValue {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "%s must have value assignment and/or body declaration", t))
|
||||
}
|
||||
if r.Body != nil && !r.generatedBody && !ruleDeclarationHasKeyword(r, tokens.If) && !r.Default {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "`if` keyword is required before %s body", t))
|
||||
}
|
||||
if r.Head.RuleKind() == MultiValue && !ruleDeclarationHasKeyword(r, tokens.Contains) {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "`contains` keyword is required for partial set rules"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
@@ -671,6 +730,15 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func ruleDeclarationHasKeyword(rule *Rule, keyword tokens.Token) bool {
|
||||
for _, kw := range rule.Head.keywords {
|
||||
if kw == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newScopeAttachmentErr(a *Annotations, want string) *Error {
|
||||
var have string
|
||||
if a.node != nil {
|
||||
@@ -686,6 +754,16 @@ func setRuleModule(rule *Rule, module *Module) {
|
||||
}
|
||||
}
|
||||
|
||||
func setJSONOptions(x interface{}, jsonOptions *astJSON.Options) {
|
||||
vis := NewGenericVisitor(func(x interface{}) bool {
|
||||
if x, ok := x.(customJSON); ok {
|
||||
x.setJSONOptions(*jsonOptions)
|
||||
}
|
||||
return false
|
||||
})
|
||||
vis.Walk(x)
|
||||
}
|
||||
|
||||
// ParserErrorDetail holds additional details for parser errors.
|
||||
type ParserErrorDetail struct {
|
||||
Line string `json:"line"`
|
||||
|
||||
+83
-24
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
@@ -42,6 +44,10 @@ var FunctionArgRootDocument = VarTerm("args")
|
||||
// features.
|
||||
var FutureRootDocument = VarTerm("future")
|
||||
|
||||
// RegoRootDocument names the document containing new, to-become-default,
|
||||
// features in a future versioned release.
|
||||
var RegoRootDocument = VarTerm("rego")
|
||||
|
||||
// RootDocumentNames contains the names of top-level documents that can be
|
||||
// referred to in modules and queries.
|
||||
//
|
||||
@@ -139,12 +145,13 @@ type (
|
||||
// within a namespace (defined by the package) and optional
|
||||
// dependencies on external documents (defined by imports).
|
||||
Module struct {
|
||||
Package *Package `json:"package"`
|
||||
Imports []*Import `json:"imports,omitempty"`
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Rules []*Rule `json:"rules,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
stmts []Statement
|
||||
Package *Package `json:"package"`
|
||||
Imports []*Import `json:"imports,omitempty"`
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Rules []*Rule `json:"rules,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
stmts []Statement
|
||||
regoV1Compatible bool
|
||||
}
|
||||
|
||||
// Comment contains the raw text from the comment in the definition.
|
||||
@@ -153,7 +160,7 @@ type (
|
||||
Text []byte
|
||||
Location *Location
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Package represents the namespace of the documents produced
|
||||
@@ -162,7 +169,7 @@ type (
|
||||
Path Ref `json:"path"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Import represents a dependency on a document outside of the policy
|
||||
@@ -172,7 +179,7 @@ type (
|
||||
Alias Var `json:"alias,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Rule represents a rule as defined in the language. Rules define the
|
||||
@@ -190,7 +197,8 @@ type (
|
||||
// on the rule (e.g., printing, comparison, visiting, etc.)
|
||||
Module *Module `json:"-"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
generatedBody bool
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Head represents the head of a rule.
|
||||
@@ -203,7 +211,9 @@ type (
|
||||
Assign bool `json:"assign,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
keywords []tokens.Token
|
||||
generatedValue bool
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Args represents zero or more arguments to a rule.
|
||||
@@ -222,7 +232,7 @@ type (
|
||||
Negated bool `json:"negated,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// SomeDecl represents a variable declaration statement. The symbols are variables.
|
||||
@@ -230,7 +240,7 @@ type (
|
||||
Symbols []*Term `json:"symbols"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
Every struct {
|
||||
@@ -240,7 +250,7 @@ type (
|
||||
Body Body `json:"body"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// With represents a modifier on an expression.
|
||||
@@ -249,7 +259,7 @@ type (
|
||||
Value *Term `json:"value"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
)
|
||||
|
||||
@@ -428,10 +438,13 @@ func (c *Comment) Equal(other *Comment) bool {
|
||||
return c.Location.Equal(other.Location) && bytes.Equal(c.Text, other.Text)
|
||||
}
|
||||
|
||||
func (c *Comment) setJSONOptions(opts JSONOptions) {
|
||||
func (c *Comment) setJSONOptions(opts astJSON.Options) {
|
||||
// Note: this is not used for location since Comments use default JSON marshaling
|
||||
// behavior with struct field names in JSON.
|
||||
c.jsonOptions = opts
|
||||
if c.Location != nil {
|
||||
c.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
// Compare returns an integer indicating whether pkg is less than, equal to,
|
||||
@@ -478,8 +491,11 @@ func (pkg *Package) String() string {
|
||||
return fmt.Sprintf("package %v", path)
|
||||
}
|
||||
|
||||
func (pkg *Package) setJSONOptions(opts JSONOptions) {
|
||||
func (pkg *Package) setJSONOptions(opts astJSON.Options) {
|
||||
pkg.jsonOptions = opts
|
||||
if pkg.Location != nil {
|
||||
pkg.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSON() ([]byte, error) {
|
||||
@@ -588,8 +604,11 @@ func (imp *Import) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (imp *Import) setJSONOptions(opts JSONOptions) {
|
||||
func (imp *Import) setJSONOptions(opts astJSON.Options) {
|
||||
imp.jsonOptions = opts
|
||||
if imp.Location != nil {
|
||||
imp.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Import) MarshalJSON() ([]byte, error) {
|
||||
@@ -699,8 +718,15 @@ func (rule *Rule) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (rule *Rule) setJSONOptions(opts JSONOptions) {
|
||||
func (rule *Rule) isFunction() bool {
|
||||
return len(rule.Head.Args) > 0
|
||||
}
|
||||
|
||||
func (rule *Rule) setJSONOptions(opts astJSON.Options) {
|
||||
rule.jsonOptions = opts
|
||||
if rule.Location != nil {
|
||||
rule.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (rule *Rule) MarshalJSON() ([]byte, error) {
|
||||
@@ -769,6 +795,17 @@ func NewHead(name Var, args ...*Term) *Head {
|
||||
return head
|
||||
}
|
||||
|
||||
// VarHead creates a head object, initializes its Name, Location, and Options,
|
||||
// and returns the new head.
|
||||
func VarHead(name Var, location *Location, jsonOpts *astJSON.Options) *Head {
|
||||
h := NewHead(name)
|
||||
h.Reference[0].Location = location
|
||||
if jsonOpts != nil {
|
||||
h.Reference[0].setJSONOptions(*jsonOpts)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// RefHead returns a new Head object with the passed Ref. If args are provided,
|
||||
// the first will be used for the value.
|
||||
func RefHead(ref Ref, args ...*Term) *Head {
|
||||
@@ -881,6 +918,7 @@ func (head *Head) Copy() *Head {
|
||||
cpy.Args = head.Args.Copy()
|
||||
cpy.Key = head.Key.Copy()
|
||||
cpy.Value = head.Value.Copy()
|
||||
cpy.keywords = nil
|
||||
return &cpy
|
||||
}
|
||||
|
||||
@@ -915,8 +953,11 @@ func (head *Head) String() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (head *Head) setJSONOptions(opts JSONOptions) {
|
||||
func (head *Head) setJSONOptions(opts astJSON.Options) {
|
||||
head.jsonOptions = opts
|
||||
if head.Location != nil {
|
||||
head.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (head *Head) MarshalJSON() ([]byte, error) {
|
||||
@@ -975,6 +1016,12 @@ func (head *Head) SetLoc(loc *Location) {
|
||||
head.Location = loc
|
||||
}
|
||||
|
||||
func (head *Head) HasDynamicRef() bool {
|
||||
pos := head.Reference.Dynamic()
|
||||
// Ref is dynamic if it has one non-constant term that isn't the first or last term or if it's a partial set rule.
|
||||
return pos > 0 && (pos < len(head.Reference)-1 || head.RuleKind() == MultiValue)
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of a.
|
||||
func (a Args) Copy() Args {
|
||||
cpy := Args{}
|
||||
@@ -1459,8 +1506,11 @@ func (expr *Expr) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (expr *Expr) setJSONOptions(opts JSONOptions) {
|
||||
func (expr *Expr) setJSONOptions(opts astJSON.Options) {
|
||||
expr.jsonOptions = opts
|
||||
if expr.Location != nil {
|
||||
expr.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (expr *Expr) MarshalJSON() ([]byte, error) {
|
||||
@@ -1555,8 +1605,11 @@ func (d *SomeDecl) Hash() int {
|
||||
return termSliceHash(d.Symbols)
|
||||
}
|
||||
|
||||
func (d *SomeDecl) setJSONOptions(opts JSONOptions) {
|
||||
func (d *SomeDecl) setJSONOptions(opts astJSON.Options) {
|
||||
d.jsonOptions = opts
|
||||
if d.Location != nil {
|
||||
d.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
|
||||
@@ -1629,8 +1682,11 @@ func (q *Every) KeyValueVars() VarSet {
|
||||
return vis.vars
|
||||
}
|
||||
|
||||
func (q *Every) setJSONOptions(opts JSONOptions) {
|
||||
func (q *Every) setJSONOptions(opts astJSON.Options) {
|
||||
q.jsonOptions = opts
|
||||
if q.Location != nil {
|
||||
q.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSON() ([]byte, error) {
|
||||
@@ -1708,8 +1764,11 @@ func (w *With) SetLoc(loc *Location) {
|
||||
w.Location = loc
|
||||
}
|
||||
|
||||
func (w *With) setJSONOptions(opts JSONOptions) {
|
||||
func (w *With) setJSONOptions(opts astJSON.Options) {
|
||||
w.jsonOptions = opts
|
||||
if w.Location != nil {
|
||||
w.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSON() ([]byte, error) {
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package ast
|
||||
|
||||
func checkDuplicateImports(modules []*Module) (errors Errors) {
|
||||
for _, module := range modules {
|
||||
processedImports := map[Var]*Import{}
|
||||
|
||||
for _, imp := range module.Imports {
|
||||
name := imp.Name()
|
||||
|
||||
if processed, conflict := processedImports[name]; conflict {
|
||||
errors = append(errors, NewError(CompileErr, imp.Location, "import must not shadow %v", processed))
|
||||
} else {
|
||||
processedImports[name] = imp
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkRootDocumentOverrides(node interface{}) Errors {
|
||||
errors := Errors{}
|
||||
|
||||
WalkRules(node, func(rule *Rule) bool {
|
||||
var name string
|
||||
if len(rule.Head.Reference) > 0 {
|
||||
name = rule.Head.Reference[0].Value.(Var).String()
|
||||
} else {
|
||||
name = rule.Head.Name.String()
|
||||
}
|
||||
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
|
||||
errors = append(errors, NewError(CompileErr, rule.Location, "rules must not shadow %v (use a different rule name)", name))
|
||||
}
|
||||
|
||||
for _, arg := range rule.Head.Args {
|
||||
if _, ok := arg.Value.(Ref); ok {
|
||||
if RootDocumentRefs.Contains(arg) {
|
||||
errors = append(errors, NewError(CompileErr, arg.Location, "args must not shadow %v (use a different variable name)", arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
WalkExprs(node, func(expr *Expr) bool {
|
||||
if expr.IsAssignment() {
|
||||
name := expr.Operand(0).String()
|
||||
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
|
||||
errors = append(errors, NewError(CompileErr, expr.Location, "variables must not shadow %v (use a different variable name)", name))
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func walkCalls(node interface{}, f func(interface{}) bool) {
|
||||
vis := &GenericVisitor{func(x interface{}) bool {
|
||||
switch x := x.(type) {
|
||||
case Call:
|
||||
return f(x)
|
||||
case *Expr:
|
||||
if x.IsCall() {
|
||||
return f(x)
|
||||
}
|
||||
case *Head:
|
||||
// GenericVisitor doesn't walk the rule head ref
|
||||
walkCalls(x.Reference, f)
|
||||
}
|
||||
return false
|
||||
}}
|
||||
vis.Walk(node)
|
||||
}
|
||||
|
||||
func checkDeprecatedBuiltins(deprecatedBuiltinsMap map[string]struct{}, node interface{}) Errors {
|
||||
errs := make(Errors, 0)
|
||||
|
||||
walkCalls(node, func(x interface{}) bool {
|
||||
var operator string
|
||||
var loc *Location
|
||||
|
||||
switch x := x.(type) {
|
||||
case *Expr:
|
||||
operator = x.Operator().String()
|
||||
loc = x.Loc()
|
||||
case Call:
|
||||
terms := []*Term(x)
|
||||
if len(terms) > 0 {
|
||||
operator = terms[0].Value.String()
|
||||
loc = terms[0].Loc()
|
||||
}
|
||||
}
|
||||
|
||||
if operator != "" {
|
||||
if _, ok := deprecatedBuiltinsMap[operator]; ok {
|
||||
errs = append(errs, NewError(TypeErr, loc, "deprecated built-in function calls in expression: %v", operator))
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func checkDeprecatedBuiltinsForCurrentVersion(node interface{}) Errors {
|
||||
deprecatedBuiltins := make(map[string]struct{})
|
||||
capabilities := CapabilitiesForThisVersion()
|
||||
for _, bi := range capabilities.Builtins {
|
||||
if bi.IsDeprecated() {
|
||||
deprecatedBuiltins[bi.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return checkDeprecatedBuiltins(deprecatedBuiltins, node)
|
||||
}
|
||||
|
||||
// CheckRegoV1 checks the given module for errors that are specific to Rego v1
|
||||
func CheckRegoV1(module *Module) Errors {
|
||||
var errors Errors
|
||||
errors = append(errors, checkDuplicateImports([]*Module{module})...)
|
||||
errors = append(errors, checkRootDocumentOverrides(module)...)
|
||||
errors = append(errors, checkDeprecatedBuiltinsForCurrentVersion(module)...)
|
||||
return errors
|
||||
}
|
||||
+65
-9
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/OneOfOne/xxhash"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/ast/location"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
@@ -294,7 +295,7 @@ type Term struct {
|
||||
Value Value `json:"value"` // the value of the Term as represented in Go
|
||||
Location *Location `json:"location,omitempty"` // the location of the Term in the source
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// NewTerm returns a new Term object.
|
||||
@@ -419,8 +420,11 @@ func (term *Term) IsGround() bool {
|
||||
return term.Value.IsGround()
|
||||
}
|
||||
|
||||
func (term *Term) setJSONOptions(opts JSONOptions) {
|
||||
func (term *Term) setJSONOptions(opts astJSON.Options) {
|
||||
term.jsonOptions = opts
|
||||
if term.Location != nil {
|
||||
term.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the term.
|
||||
@@ -888,8 +892,8 @@ func PtrRef(head *Term, s string) (Ref, error) {
|
||||
return Ref{head}, nil
|
||||
}
|
||||
parts := strings.Split(s, "/")
|
||||
if max := math.MaxInt32; len(parts) >= max {
|
||||
return nil, fmt.Errorf("path too long: %s, %d > %d (max)", s, len(parts), max)
|
||||
if maxLen := math.MaxInt32; len(parts) >= maxLen {
|
||||
return nil, fmt.Errorf("path too long: %s, %d > %d (max)", s, len(parts), maxLen)
|
||||
}
|
||||
ref := make(Ref, uint(len(parts))+1)
|
||||
ref[0] = head
|
||||
@@ -1028,6 +1032,20 @@ func (ref Ref) ConstantPrefix() Ref {
|
||||
return ref[:i]
|
||||
}
|
||||
|
||||
func (ref Ref) StringPrefix() Ref {
|
||||
r := ref.Copy()
|
||||
|
||||
for i := 1; i < len(ref); i++ {
|
||||
switch r[i].Value.(type) {
|
||||
case String: // pass
|
||||
default: // cut off
|
||||
return r[:i]
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// GroundPrefix returns the ground portion of the ref starting from the head. By
|
||||
// definition, the head of the reference is always ground.
|
||||
func (ref Ref) GroundPrefix() Ref {
|
||||
@@ -1043,6 +1061,14 @@ func (ref Ref) GroundPrefix() Ref {
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (ref Ref) DynamicSuffix() Ref {
|
||||
i := ref.Dynamic()
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return ref[i:]
|
||||
}
|
||||
|
||||
// IsGround returns true if all of the parts of the Ref are ground.
|
||||
func (ref Ref) IsGround() bool {
|
||||
if len(ref) == 0 {
|
||||
@@ -1078,6 +1104,10 @@ func (ref Ref) Ptr() (string, error) {
|
||||
|
||||
var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
|
||||
|
||||
func IsVarCompatibleString(s string) bool {
|
||||
return varRegexp.MatchString(s)
|
||||
}
|
||||
|
||||
func (ref Ref) String() string {
|
||||
if len(ref) == 0 {
|
||||
return ""
|
||||
@@ -1840,22 +1870,34 @@ func ObjectTerm(o ...[2]*Term) *Term {
|
||||
}
|
||||
|
||||
func LazyObject(blob map[string]interface{}) Object {
|
||||
return &lazyObj{native: blob}
|
||||
return &lazyObj{native: blob, cache: map[string]Value{}}
|
||||
}
|
||||
|
||||
type lazyObj struct {
|
||||
strict Object
|
||||
cache map[string]Value
|
||||
native map[string]interface{}
|
||||
}
|
||||
|
||||
func (l *lazyObj) force() Object {
|
||||
if l.strict == nil {
|
||||
l.strict = MustInterfaceToValue(l.native).(Object)
|
||||
// NOTE(jf): a possible performance improvement here would be to check how many
|
||||
// entries have been realized to AST in the cache, and if some threshold compared to the
|
||||
// total number of keys is exceeded, realize the remaining entries and set l.strict to l.cache.
|
||||
l.cache = map[string]Value{} // We don't need the cache anymore; drop it to free up memory.
|
||||
}
|
||||
return l.strict
|
||||
}
|
||||
|
||||
func (l *lazyObj) Compare(other Value) int {
|
||||
o1 := sortOrder(l)
|
||||
o2 := sortOrder(other)
|
||||
if o1 < o2 {
|
||||
return -1
|
||||
} else if o2 < o1 {
|
||||
return 1
|
||||
}
|
||||
return l.force().Compare(other)
|
||||
}
|
||||
|
||||
@@ -1924,13 +1966,20 @@ func (l *lazyObj) Get(k *Term) *Term {
|
||||
return l.strict.Get(k)
|
||||
}
|
||||
if s, ok := k.Value.(String); ok {
|
||||
if v, ok := l.cache[string(s)]; ok {
|
||||
return NewTerm(v)
|
||||
}
|
||||
|
||||
if val, ok := l.native[string(s)]; ok {
|
||||
var converted Value
|
||||
switch val := val.(type) {
|
||||
case map[string]interface{}:
|
||||
return NewTerm(&lazyObj{native: val})
|
||||
converted = LazyObject(val)
|
||||
default:
|
||||
return NewTerm(MustInterfaceToValue(val))
|
||||
converted = MustInterfaceToValue(val)
|
||||
}
|
||||
l.cache[string(s)] = converted
|
||||
return NewTerm(converted)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -1985,13 +2034,20 @@ func (l *lazyObj) Find(path Ref) (Value, error) {
|
||||
return l, nil
|
||||
}
|
||||
if p0, ok := path[0].Value.(String); ok {
|
||||
if v, ok := l.cache[string(p0)]; ok {
|
||||
return v.Find(path[1:])
|
||||
}
|
||||
|
||||
if v, ok := l.native[string(p0)]; ok {
|
||||
var converted Value
|
||||
switch v := v.(type) {
|
||||
case map[string]interface{}:
|
||||
return (&lazyObj{native: v}).Find(path[1:])
|
||||
converted = LazyObject(v)
|
||||
default:
|
||||
return MustInterfaceToValue(v).Find(path[1:])
|
||||
converted = MustInterfaceToValue(v)
|
||||
}
|
||||
l.cache[string(p0)] = converted
|
||||
return converted.Find(path[1:])
|
||||
}
|
||||
}
|
||||
return nil, errFindNotFound
|
||||
|
||||
+1429
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user