// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 //go:build go1.18 // +build go1.18 package hclsyntax import ( "fmt" "testing" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" ) // This file contains some additional tests that only make sense when using // a Go compiler which supports type parameters (Go 1.18 or later). func TestExpressionDiagnosticExtra(t *testing.T) { tests := []struct { input string 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()", &hcl.EvalContext{ Functions: map[string]function.Function{ "boop": 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) { try := func(diags hcl.Diagnostics) { t.Helper() for _, diag := range diags { extra, ok := hcl.DiagnosticExtra[FunctionCallDiagExtra](diag) if !ok { continue } if got, want := extra.CalledFunctionName(), "boop"; got != want { t.Errorf("wrong called function name %q; want %q", got, want) } err := extra.FunctionCallError() if err == nil { t.Fatal("FunctionCallError returned nil") } if got, want := err.Error(), "the expected error"; got != want { t.Errorf("wrong error message\ngot: %q\nwant: %q", got, want) } return } t.Fatalf("None of the returned diagnostics implement FunctionCallDiagError\n%s", diags.Error()) } t.Run("unwrapped", func(t *testing.T) { try(diags) }) // It should also work if we wrap up the "extras" in wrapper types. for _, diag := range diags { diag.Extra = diagnosticExtraWrapper{diag.Extra} } t.Run("wrapped", func(t *testing.T) { try(diags) }) }, }, } for _, test := range tests { t.Run(test.input, func(t *testing.T) { var diags hcl.Diagnostics expr, parseDiags := ParseExpression([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) diags = append(diags, parseDiags...) _, valDiags := expr.Value(test.ctx) diags = append(diags, valDiags...) if !diags.HasErrors() { t.Fatal("unexpected success") } test.assert(t, diags) }) } } type diagnosticExtraWrapper struct { wrapped interface{} } var _ hcl.DiagnosticExtraUnwrapper = diagnosticExtraWrapper{} func (w diagnosticExtraWrapper) UnwrapDiagnosticExtra() interface{} { return w.wrapped }