summaryrefslogtreecommitdiff
path: root/hclsyntax/expression.go
diff options
context:
space:
mode:
authorMartin Atkins <mart@degeneration.co.uk>2022-06-09 16:29:19 -0700
committerJames Bardin <j.bardin@gmail.com>2023-10-24 10:54:03 -0400
commit83c95d29eef525594a70dd05027d93ff2f8f7c55 (patch)
tree8f71f3f29c232535af4aded55860fe4190496b5f /hclsyntax/expression.go
parentd23a20ac41349e5f1cc7b60e76d1408069db498b (diff)
hclsyntax: Initial work on namespaced functions
This introduces a new syntax which allows function names to have namespace prefixes, with the different name parts separated by a double-colon "::" as is common in various other C-derived languages which need to distinguish between scope resolution and attribute/field traversal. Because HCL has separate namespaces for functions and variables, we need to use different punctuation for each to avoid creating parsing ambiguity that could be resolved only with infinite lookahead. We cannot retroactively change the representation of function names to be a slice of names without breaking the existing API, and so we instead adopt a convention of packing the multi-part names into single strings which the parser guarantees will always be a series of valid identifiers separated by the literal "::" sequence. That means that applications will make namespaced functions available in the EvalContext by naming them in a way that matches this convention. This is still a subtle compatibility break for any implementation of the syntax-agnostic HCL API against another syntax, because it may now encounter function names in the function table that are not entirely valid identifiers. However, that's okay in practice because a calling application is always in full control of both which syntaxes it supports and which functions it places in the function table, and so an application using some other syntax can simply avoid using namespaced functions until that syntax is updated to understand the new convention. This initial commit only includes the basic functionality and does not yet update the specification or specification test suite. It also has only minimal unit tests of the parser and evaluator. Before finalizing this in a release we would need to complete that work to make sure everything is consistent and that we have sufficient regression tests for this new capability.
Diffstat (limited to 'hclsyntax/expression.go')
-rw-r--r--hclsyntax/expression.go62
1 files changed, 62 insertions, 0 deletions
diff --git a/hclsyntax/expression.go b/hclsyntax/expression.go
index e0de1c3..e81553b 100644
--- a/hclsyntax/expression.go
+++ b/hclsyntax/expression.go
@@ -6,6 +6,7 @@ package hclsyntax
import (
"fmt"
"sort"
+ "strings"
"sync"
"github.com/hashicorp/hcl/v2"
@@ -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)