Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ast/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ func (p *propertyAccessParser) peek() (byte, bool) {

// Parses a property access. See the comment on parsePropertyAccess for the grammar and examples.
func (p *propertyAccessParser) parse() (int, string, *PropertyAccess, syntax.Diagnostics) {
if c, ok := p.peek(); ok && c == '.' {
// treating an empty initial property name (which is otherwise illegal) as access to the document root
// (conceptually the first property for `.foo` is empty)
p.append(&PropertyName{Name: "", AccessorRange: p.rangeFrom(p.offset)})
}

for {
c, ok := p.peek()
if !ok {
Expand Down
60 changes: 43 additions & 17 deletions eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,27 +719,18 @@ func (e *evalContext) evaluatePropertyAccess(x *expr, accessors []*propertyAcces
// is any other type of expression, it is evaluated and the result is passed to evaluateValueAccess. Once all accessors
// have been processed, the resolved expression is evaluated.
func (e *evalContext) evaluateExprAccess(x *expr, accessors []*propertyAccessor, accept *schema.Schema) *value {
receiver := e.root

k, ok := e.objectKey(x.repr.syntax(), accessors[0].accessor, false)

// Check for an imports access.
if ok && k == "imports" {
accessors[0].value = e.myImports
return e.evaluateValueAccess(x.repr.syntax(), e.myImports, accessors[1:])
// root access
if ok && k == "" {
return e.evaluateRootAccess(x, accessors[1:], accept)
}

// Check for context interpolation.
if ok && k == "context" {
accessors[0].value = e.myContext
return e.evaluateValueAccess(x.repr.syntax(), e.myContext, accessors[1:])
}

// Check for inline reference
if ok && k == "environments" {
return e.evaluateEnvironmentReferenceAccess(x, accessors, accept)
// reserved top-level key access -- these keys are reserved within the context of `values`
if ok && e.isReserveTopLevelKey(k) {
return e.evaluateRootAccess(x, accessors, accept)
}

receiver := e.root
for len(accessors) > 0 {
accessor := accessors[0]
if receiver == nil {
Expand Down Expand Up @@ -792,9 +783,44 @@ func (e *evalContext) evaluateExprAccess(x *expr, accessors []*propertyAccessor,
return e.evaluateExpr(receiver, schema.Always())
}

// evaluateRootAccess evaluates access to the reserved keys "mounted" at document root
func (e *evalContext) evaluateRootAccess(x *expr, accessors []*propertyAccessor, accept *schema.Schema) *value {
k, ok := e.objectKey(x.repr.syntax(), accessors[0].accessor, false)

// Check for an imports access.
if ok && k == "imports" {
accessors[0].value = e.myImports
return e.evaluateValueAccess(x.repr.syntax(), e.myImports, accessors[1:])
}

// Check for context interpolation.
if ok && k == "context" {
accessors[0].value = e.myContext
return e.evaluateValueAccess(x.repr.syntax(), e.myContext, accessors[1:])
}

// Check for environment reference
if ok && k == "environments" {
return e.evaluateEnvironmentReferenceAccess(x, accessors)
}

// Check for values access.
// There isn't really any reason for a user to do this, because ${.values.foo} === ${foo}, it's just for consistency
if ok && k == "values" {
if len(accessors) == 1 {
// referencing ${.values} is inherently cyclical
e.errorf(x.repr.syntax(), "cyclic reference to values")
return &value{def: x, schema: schema.Always().Schema(), unknown: true}
}
return e.evaluateExprAccess(x, accessors[1:], accept)
}

return e.invalidPropertyAccess(x.repr.syntax(), accessors)
}

// evaluateEnvironmentReferenceAccess performs an inline import of an environment.
// The accessor is of the form ["environments", $project, $env, ...], which is transformed into an import name in the form "$project/$env"
func (e *evalContext) evaluateEnvironmentReferenceAccess(x *expr, accessors []*propertyAccessor, accept *schema.Schema) *value {
func (e *evalContext) evaluateEnvironmentReferenceAccess(x *expr, accessors []*propertyAccessor) *value {
// desugar accessor path into import name
if len(accessors) < 3 {
// need at least the first three elements to create an import name
Expand Down
13 changes: 13 additions & 0 deletions eval/testdata/eval/access-root/env.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
imports:
- foo/bar

values:
foo: "hello!"

imp: ${.imports}
ctx: ${.context}
env: ${.environments}
env2: ${.environments.foo.bar.some_value}
values: ${.values}
values2: ${.values.foo}
invalid: ${.invalid}
Loading
Loading