Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaced functions #639

Merged
merged 4 commits into from Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions hclsyntax/expression.go
Expand Up @@ -6,6 +6,7 @@ package hclsyntax
import (
"fmt"
"sort"
"strings"
"sync"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -251,6 +252,67 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
}
}

// For historical reasons, we represent namespaced function names
// as strings with :: separating the names. If this was an attempt
// to call a namespaced function then we'll try to distinguish
// between an invalid namespace or an invalid name within a valid
// namespace in order to give the user better feedback about what
// is wrong.
//
// The parser guarantees that a function name will always
// be a series of valid identifiers separated by "::" with no
// other content, so we can be relatively unforgiving in our processing
// here.
if sepIdx := strings.LastIndex(e.Name, "::"); sepIdx != -1 {
namespace := e.Name[:sepIdx+2]
name := e.Name[sepIdx+2:]

avail := make([]string, 0, len(ctx.Functions))
for availName := range ctx.Functions {
if strings.HasPrefix(availName, namespace) {
avail = append(avail, availName)
}
}

if len(avail) == 0 {
// TODO: Maybe use nameSuggestion for the other available
// namespaces? But that'd require us to go scan the function
// table again, so we'll wait to see if it's really warranted.
// For now, we're assuming people are more likely to misremember
// the function names than the namespaces, because in many
// applications there will be relatively few namespaces compared
// to the number of distinct functions.
return cty.DynamicVal, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Call to unknown function",
Detail: fmt.Sprintf("There are no functions in namespace %q.", namespace),
Subject: &e.NameRange,
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
} else {
suggestion := nameSuggestion(name, avail)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %s%s?", namespace, suggestion)
}

return cty.DynamicVal, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Call to unknown function",
Detail: fmt.Sprintf("There is no function named %q in namespace %s.%s", name, namespace, suggestion),
Subject: &e.NameRange,
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
},
}
}
}

avail := make([]string, 0, len(ctx.Functions))
for name := range ctx.Functions {
avail = append(avail, name)
Expand Down
56 changes: 55 additions & 1 deletion hclsyntax/expression_test.go
Expand Up @@ -342,6 +342,56 @@ upper(
cty.DynamicVal,
0,
},
{
`foo::upper("foo")`,
&hcl.EvalContext{
Functions: map[string]function.Function{
"foo::upper": stdlib.UpperFunc,
},
},
cty.StringVal("FOO"),
0,
},
{
`foo :: upper("foo")`, // spaces are non-idomatic, but valid
radeksimko marked this conversation as resolved.
Show resolved Hide resolved
&hcl.EvalContext{
Functions: map[string]function.Function{
"foo::upper": stdlib.UpperFunc,
},
},
cty.StringVal("FOO"),
0,
},
{
`::upper("foo")`, // :: is still not a valid identifier
&hcl.EvalContext{
Functions: map[string]function.Function{
"::upper": stdlib.UpperFunc,
},
},
cty.DynamicVal,
1,
},
{
`double::::upper("foo")`, // missing name after ::
&hcl.EvalContext{
Functions: map[string]function.Function{
"double::::upper": stdlib.UpperFunc,
},
},
cty.NilVal,
1,
},
{
`missing::("foo")`, // missing name after ::
&hcl.EvalContext{
Functions: map[string]function.Function{
"missing::": stdlib.UpperFunc,
},
},
cty.NilVal,
1,
},
{
`misbehave()`,
&hcl.EvalContext{
Expand Down Expand Up @@ -2174,8 +2224,12 @@ EOT
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
expr, parseDiags := ParseExpression([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
var got cty.Value
var valDiags hcl.Diagnostics

got, valDiags := expr.Value(test.ctx)
if expr != nil {
dbanck marked this conversation as resolved.
Show resolved Hide resolved
got, valDiags = expr.Value(test.ctx)
}

diagCount := len(parseDiags) + len(valDiags)

Expand Down
2 changes: 1 addition & 1 deletion hclsyntax/generate.go
Expand Up @@ -9,4 +9,4 @@ package hclsyntax
//go:generate gofmt -w scan_tokens.go
//go:generate ragel -Z scan_string_lit.rl
//go:generate gofmt -w scan_string_lit.go
//go:generate stringer -type TokenType -output token_type_string.go
//go:generate go run golang.org/x/tools/cmd/stringer -type TokenType -output token_type_string.go
53 changes: 47 additions & 6 deletions hclsyntax/parser.go
Expand Up @@ -999,7 +999,7 @@ func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) {
case TokenIdent:
tok := p.Read() // eat identifier token

if p.Peek().Type == TokenOParen {
if p.Peek().Type == TokenOParen || p.Peek().Type == TokenDoubleColon {
return p.finishParsingFunctionCall(tok)
}

Expand Down Expand Up @@ -1145,16 +1145,57 @@ func (p *parser) numberLitValue(tok Token) (cty.Value, hcl.Diagnostics) {

// finishParsingFunctionCall parses a function call assuming that the function
// name was already read, and so the peeker should be pointing at the opening
// parenthesis after the name.
// parenthesis after the name, or at the double-colon after the initial
// function scope name.
func (p *parser) finishParsingFunctionCall(name Token) (Expression, hcl.Diagnostics) {
var diags hcl.Diagnostics

openTok := p.Read()
if openTok.Type != TokenOParen {
if openTok.Type != TokenOParen && openTok.Type != TokenDoubleColon {
// should never happen if callers behave
panic("finishParsingFunctionCall called with non-parenthesis as next token")
panic("finishParsingFunctionCall called with unsupported next token")
}

nameStr := string(name.Bytes)
for openTok.Type == TokenDoubleColon {
nextName := p.Read()
if nextName.Type != TokenIdent {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing function name",
Detail: "Function scope resolution symbol :: must be followed by a function name in this scope.",
Subject: &nextName.Range,
Context: hcl.RangeBetween(name.Range, nextName.Range).Ptr(),
})
p.recoverOver(TokenOParen)
return nil, diags
}

// Initial versions of HCLv2 didn't support function namespaces, and
// so for backward compatibility we just treat namespaced functions
// as weird names with "::" separators in them, saved as a string
// to keep the API unchanged. FunctionCallExpr also has some special
// handling of names containing :: when referring to a function that
// doesn't exist in EvalContext, to return better error messages
// when namespaces are used incorrectly.
nameStr = nameStr + "::" + string(nextName.Bytes)

openTok = p.Read()
}

if openTok.Type != TokenOParen {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing open parenthesis",
Detail: "Function selector must be followed by an open parenthesis to begin the function call.",
Subject: &openTok.Range,
Context: hcl.RangeBetween(name.Range, openTok.Range).Ptr(),
})
p.recoverOver(TokenOParen)
return nil, diags
}

var args []Expression
var diags hcl.Diagnostics
var expandFinal bool
var closeTok Token

Expand Down Expand Up @@ -1245,7 +1286,7 @@ Token:
p.PopIncludeNewlines()

return &FunctionCallExpr{
Name: string(name.Bytes),
Name: nameStr,
Args: args,

ExpandFinal: expandFinal,
Expand Down