Skip to content

Commit

Permalink
Create an error type for unknown function diags
Browse files Browse the repository at this point in the history
Now that we have namespaced functions, and implementations like
Terraform can add functions based on configuration, the reason for an
unknown function call name becomes a little less clear. Because
functions are populated outside of the hcl package scope, there isn't
enough context to provide a useful diagnostic to the user.

We can create a new Diagnostic.Extra value for
FunctionCallUnknownDiagExtra to indicate specifically when a diagnostic
is created due to an unknown function name. This will carry back the
namespace and function name for the caller to inspect, which will allow
refinement of the diagnostic based on information only known to the
caller.
  • Loading branch information
jbardin committed Feb 14, 2024
1 parent 1e1a6b8 commit 159a39d
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 0 deletions.
31 changes: 31 additions & 0 deletions hclsyntax/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
}
}

extraUnknown := &functionCallUnknown{
name: e.Name,
}

// 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
Expand All @@ -274,6 +278,9 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
}
}

extraUnknown.name = name
extraUnknown.namespace = namespace

if len(avail) == 0 {
// TODO: Maybe use nameSuggestion for the other available
// namespaces? But that'd require us to go scan the function
Expand All @@ -291,6 +298,7 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
Extra: extraUnknown,
},
}
} else {
Expand All @@ -308,6 +316,7 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
Extra: extraUnknown,
},
}
}
Expand All @@ -331,6 +340,7 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
Context: e.Range().Ptr(),
Expression: e,
EvalContext: ctx,
Extra: extraUnknown,
},
}
}
Expand Down Expand Up @@ -678,6 +688,27 @@ func (e *functionCallDiagExtra) FunctionCallError() error {
return e.functionCallError
}

// FunctionCallUnknownDiagExtra is an interface implemented by a value in the Extra
// field of some diagnostics to indicate when the error was caused by a call to
// an unknown function.
type FunctionCallUnknownDiagExtra interface {
CalledFunctionName() string
CalledFunctionNamespace() string
}

type functionCallUnknown struct {
name string
namespace string
}

func (e *functionCallUnknown) CalledFunctionName() string {
return e.name
}

func (e *functionCallUnknown) CalledFunctionNamespace() string {
return e.namespace
}

type ConditionalExpr struct {
Condition Expression
TrueResult Expression
Expand Down
65 changes: 65 additions & 0 deletions hclsyntax/expression_typeparams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,71 @@ func TestExpressionDiagnosticExtra(t *testing.T) {
ctx *hcl.EvalContext
assert func(t *testing.T, diags hcl.Diagnostics)
}{
// Errors for unknown function calls
{
"boop()",
&hcl.EvalContext{
Functions: map[string]function.Function{
"zap": function.New(&function.Spec{
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.DynamicVal, fmt.Errorf("the expected error")
},
}),
},
},
func(t *testing.T, diags hcl.Diagnostics) {
t.Helper()
for _, diag := range diags {
extra, ok := hcl.DiagnosticExtra[FunctionCallUnknownDiagExtra](diag)
if !ok {
continue
}

if got, want := extra.CalledFunctionName(), "boop"; got != want {
t.Errorf("wrong called function name %q; want %q", got, want)
}
ns := extra.CalledFunctionNamespace()
if ns != "" {
t.Fatal("expected no namespace, got", ns)
}
return
}
t.Fatalf("None of the returned diagnostics implement FunctionCallUnknownDiagExtra\n%s", diags.Error())
},
},
{
"ns::source::boop()",
&hcl.EvalContext{
Functions: map[string]function.Function{
"zap": function.New(&function.Spec{
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.DynamicVal, fmt.Errorf("the expected error")
},
}),
},
},
func(t *testing.T, diags hcl.Diagnostics) {
t.Helper()
for _, diag := range diags {
extra, ok := hcl.DiagnosticExtra[FunctionCallUnknownDiagExtra](diag)
if !ok {
continue
}

if got, want := extra.CalledFunctionName(), "boop"; got != want {
t.Errorf("wrong called function name %q; want %q", got, want)
}
ns := extra.CalledFunctionNamespace()
if ns != "ns::source::" {
t.Fatal("expected namespace ns::source::, got", ns)
}
return
}
t.Fatalf("None of the returned diagnostics implement FunctionCallUnknownDiagExtra\n%s", diags.Error())
},
},
// Error messages describing inconsistent result types for conditional expressions.
{
"boop()",
Expand Down

0 comments on commit 159a39d

Please sign in to comment.