mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-25 17:55:15 -06:00
3672a3f892
Previously, if callers omitted output terms from call expressions, the
result would be ignored. This was fine for most calls which would only
return true if they were defined, however, for functions could return
false this became confusing because an expression like "f(1)" where f(1)
= false would be succeed and yield a result.
With these changes, built-in functions that used to only return true
always return true or false and the eval engine takes care to check if
the result is false when the the caller omits the output term.
This provides consistent behaviour across cases like...
f(1) => undefined (previously {})
f(1,x) => {x:false} (previously {x:false})
neq(1,1,x) => {x:false} (previously undefined)
neq(1,1,false) => {} (previously undefined)
neq(1,1,true) => undefined (previously undefined)
In the next set of changes, the Rego package will be updated to capture
values for expressions like the first one above so that function calls
behave like refs (i.e., their values are returned).
324 lines
6.6 KiB
Go
324 lines
6.6 KiB
Go
// Copyright 2017 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ast
|
|
|
|
import (
|
|
"github.com/open-policy-agent/opa/types"
|
|
"github.com/open-policy-agent/opa/util"
|
|
)
|
|
|
|
// TypeEnv contains type info for static analysis such as type checking.
|
|
type TypeEnv struct {
|
|
tree *typeTreeNode
|
|
next *TypeEnv
|
|
}
|
|
|
|
// NewTypeEnv returns an empty TypeEnv.
|
|
func NewTypeEnv() *TypeEnv {
|
|
return &TypeEnv{
|
|
tree: newTypeTree(),
|
|
}
|
|
}
|
|
|
|
// Get returns the type of x.
|
|
func (env *TypeEnv) Get(x interface{}) types.Type {
|
|
|
|
if term, ok := x.(*Term); ok {
|
|
x = term.Value
|
|
}
|
|
|
|
switch x := x.(type) {
|
|
|
|
// Scalars.
|
|
case Null:
|
|
return types.NewNull()
|
|
case Boolean:
|
|
return types.NewBoolean()
|
|
case Number:
|
|
return types.NewNumber()
|
|
case String:
|
|
return types.NewString()
|
|
|
|
// Composites.
|
|
case Array:
|
|
static := make([]types.Type, len(x))
|
|
for i := range static {
|
|
tpe := env.Get(x[i].Value)
|
|
static[i] = tpe
|
|
}
|
|
|
|
var dynamic types.Type
|
|
if len(static) == 0 {
|
|
dynamic = types.A
|
|
}
|
|
|
|
return types.NewArray(static, dynamic)
|
|
|
|
case Object:
|
|
static := []*types.StaticProperty{}
|
|
var dynamic *types.DynamicProperty
|
|
|
|
x.Foreach(func(k, v *Term) {
|
|
if IsConstant(k.Value) {
|
|
kjson, err := JSON(k.Value)
|
|
if err != nil {
|
|
panic("unreachable")
|
|
}
|
|
tpe := env.Get(v)
|
|
static = append(static, types.NewStaticProperty(kjson, tpe))
|
|
} else {
|
|
typeK := env.Get(k.Value)
|
|
typeV := env.Get(v.Value)
|
|
dynamic = types.NewDynamicProperty(typeK, typeV)
|
|
}
|
|
})
|
|
|
|
if len(static) == 0 && dynamic == nil {
|
|
dynamic = types.NewDynamicProperty(types.A, types.A)
|
|
}
|
|
|
|
return types.NewObject(static, dynamic)
|
|
|
|
case Set:
|
|
var tpe types.Type
|
|
x.Foreach(func(elem *Term) {
|
|
other := env.Get(elem.Value)
|
|
tpe = types.Or(tpe, other)
|
|
})
|
|
if tpe == nil {
|
|
tpe = types.A
|
|
}
|
|
return types.NewSet(tpe)
|
|
|
|
// Comprehensions.
|
|
case *ArrayComprehension:
|
|
checker := newTypeChecker()
|
|
cpy, errs := checker.CheckBody(env, x.Body)
|
|
if len(errs) == 0 {
|
|
return types.NewArray(nil, cpy.Get(x.Term))
|
|
}
|
|
return nil
|
|
case *ObjectComprehension:
|
|
checker := newTypeChecker()
|
|
cpy, errs := checker.CheckBody(env, x.Body)
|
|
if len(errs) == 0 {
|
|
return types.NewObject(nil, types.NewDynamicProperty(cpy.Get(x.Key), cpy.Get(x.Value)))
|
|
}
|
|
return nil
|
|
case *SetComprehension:
|
|
checker := newTypeChecker()
|
|
cpy, errs := checker.CheckBody(env, x.Body)
|
|
if len(errs) == 0 {
|
|
return types.NewSet(cpy.Get(x.Term))
|
|
}
|
|
return nil
|
|
|
|
// Refs.
|
|
case Ref:
|
|
return env.getRef(x)
|
|
|
|
// Vars.
|
|
case Var:
|
|
if node := env.tree.Child(x); node != nil {
|
|
return node.Value()
|
|
}
|
|
if env.next != nil {
|
|
return env.next.Get(x)
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
func (env *TypeEnv) getRef(ref Ref) types.Type {
|
|
|
|
node := env.tree.Child(ref[0].Value)
|
|
if node == nil {
|
|
return env.getRefFallback(ref)
|
|
}
|
|
|
|
return env.getRefRec(node, ref, ref[1:])
|
|
}
|
|
|
|
func (env *TypeEnv) getRefFallback(ref Ref) types.Type {
|
|
|
|
if env.next != nil {
|
|
return env.next.Get(ref)
|
|
}
|
|
|
|
if RootDocumentNames.Contains(ref[0]) {
|
|
return types.A
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (env *TypeEnv) getRefRec(node *typeTreeNode, ref, tail Ref) types.Type {
|
|
if len(tail) == 0 {
|
|
return env.getRefRecExtent(node)
|
|
}
|
|
|
|
if node.Leaf() {
|
|
return selectRef(node.Value(), tail)
|
|
}
|
|
|
|
if !IsConstant(tail[0].Value) {
|
|
return selectRef(env.getRefRecExtent(node), tail)
|
|
}
|
|
|
|
child := node.Child(tail[0].Value)
|
|
if child == nil {
|
|
return env.getRefFallback(ref)
|
|
}
|
|
|
|
return env.getRefRec(child, ref, tail[1:])
|
|
}
|
|
|
|
func (env *TypeEnv) getRefRecExtent(node *typeTreeNode) types.Type {
|
|
|
|
if node.Leaf() {
|
|
return node.Value()
|
|
}
|
|
|
|
children := []*types.StaticProperty{}
|
|
|
|
node.Children().Iter(func(k, v util.T) bool {
|
|
key := k.(Value)
|
|
child := v.(*typeTreeNode)
|
|
|
|
tpe := env.getRefRecExtent(child)
|
|
// TODO(tsandall): handle non-string keys?
|
|
if s, ok := key.(String); ok {
|
|
children = append(children, types.NewStaticProperty(string(s), tpe))
|
|
}
|
|
return false
|
|
})
|
|
|
|
// TODO(tsandall): for now, these objects can have any dynamic properties
|
|
// because we don't have schema for base docs. Once schemas are supported
|
|
// we can improve this.
|
|
return types.NewObject(children, types.NewDynamicProperty(types.S, types.A))
|
|
}
|
|
|
|
func (env *TypeEnv) wrap() *TypeEnv {
|
|
cpy := *env
|
|
cpy.next = env
|
|
cpy.tree = newTypeTree()
|
|
return &cpy
|
|
}
|
|
|
|
// typeTreeNode is used to store type information in a tree.
|
|
type typeTreeNode struct {
|
|
key Value
|
|
value types.Type
|
|
children *util.HashMap
|
|
}
|
|
|
|
func newTypeTree() *typeTreeNode {
|
|
return &typeTreeNode{
|
|
key: nil,
|
|
value: nil,
|
|
children: util.NewHashMap(valueEq, valueHash),
|
|
}
|
|
}
|
|
|
|
func (n *typeTreeNode) Child(key Value) *typeTreeNode {
|
|
value, ok := n.children.Get(key)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return value.(*typeTreeNode)
|
|
}
|
|
|
|
func (n *typeTreeNode) Children() *util.HashMap {
|
|
return n.children
|
|
}
|
|
|
|
func (n *typeTreeNode) Get(path Ref) types.Type {
|
|
curr := n
|
|
for _, term := range path {
|
|
child, ok := curr.children.Get(term.Value)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
curr = child.(*typeTreeNode)
|
|
}
|
|
return curr.Value()
|
|
}
|
|
|
|
func (n *typeTreeNode) Leaf() bool {
|
|
return n.value != nil
|
|
}
|
|
|
|
func (n *typeTreeNode) PutOne(key Value, tpe types.Type) {
|
|
c, ok := n.children.Get(key)
|
|
|
|
var child *typeTreeNode
|
|
if !ok {
|
|
child = newTypeTree()
|
|
child.key = key
|
|
n.children.Put(key, child)
|
|
} else {
|
|
child = c.(*typeTreeNode)
|
|
}
|
|
|
|
child.value = tpe
|
|
}
|
|
|
|
func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
|
|
curr := n
|
|
for _, 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)
|
|
}
|
|
|
|
curr = child
|
|
}
|
|
curr.value = tpe
|
|
}
|
|
|
|
func (n *typeTreeNode) Value() types.Type {
|
|
return n.value
|
|
}
|
|
|
|
// selectConstant returns the attribute of the type referred to by the term. If
|
|
// the attribute type cannot be determined, nil is returned.
|
|
func selectConstant(tpe types.Type, term *Term) types.Type {
|
|
x, err := JSON(term.Value)
|
|
if err == nil {
|
|
return types.Select(tpe, x)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// selectRef returns the type of the nested attribute referred to by ref. If
|
|
// the attribute type cannot be determined, nil is returned. If the ref
|
|
// contains vars or refs, then the returned type will be a union of the
|
|
// possible types.
|
|
func selectRef(tpe types.Type, ref Ref) types.Type {
|
|
|
|
if tpe == nil || len(ref) == 0 {
|
|
return tpe
|
|
}
|
|
|
|
head, tail := ref[0], ref[1:]
|
|
|
|
switch head.Value.(type) {
|
|
case Var, Ref, Array, Object, Set:
|
|
return selectRef(types.Values(tpe), tail)
|
|
default:
|
|
return selectRef(selectConstant(tpe, head), tail)
|
|
}
|
|
}
|