diff --git a/internal/repl/format.go b/internal/repl/format.go index 70fb66abf..fbdd44f58 100644 --- a/internal/repl/format.go +++ b/internal/repl/format.go @@ -17,10 +17,6 @@ func FormatValue(v cty.Value, indent int) string { if !v.IsKnown() { return "(known after apply)" } - if v.Type().Equals(cty.String) && v.HasMark(marks.Raw) { - raw, _ := v.Unmark() - return raw.AsString() - } if v.HasMark(marks.Sensitive) { return "(sensitive)" } diff --git a/internal/repl/session.go b/internal/repl/session.go index a9b7b1b12..41a6c359b 100644 --- a/internal/repl/session.go +++ b/internal/repl/session.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -54,6 +55,29 @@ func (s *Session) handleEval(line string) (string, tfdiags.Diagnostics) { return "", diags } + // The raw mark is used only by the console-only `type` function, in order + // to allow display of a string value representation of the type without the + // usual HCL formatting. If we receive a string value with this mark, we do + // not want to format it any further. + // + // Due to mark propagation in cty, calling `type` as part of a larger + // expression can lead to other values being marked, which can in turn lead + // to unpredictable results. If any non-string value has the raw mark, we + // return a diagnostic explaining that this use of `type` is not permitted. + if marks.Contains(val, marks.Raw) { + if val.Type().Equals(cty.String) { + raw, _ := val.Unmark() + return raw.AsString(), diags + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid use of type function", + "The console-only \"type\" function cannot be used as part of an expression.", + )) + return "", diags + } + } + return FormatValue(val, 0), diags } diff --git a/internal/repl/session_test.go b/internal/repl/session_test.go index 7110324e1..bf060f607 100644 --- a/internal/repl/session_test.go +++ b/internal/repl/session_test.go @@ -120,6 +120,20 @@ func TestSession_basicState(t *testing.T) { }, }) }) + + t.Run("type function", func(t *testing.T) { + testSession(t, testSessionTest{ + State: state, + Inputs: []testSessionInput{ + { + Input: "type(test_instance.foo)", + Output: `object({ + id: string, +})`, + }, + }, + }) + }) } func TestSession_stateless(t *testing.T) { @@ -178,6 +192,18 @@ func TestSession_stateless(t *testing.T) { }, }) }) + + t.Run("type function cannot be used in expressions", func(t *testing.T) { + testSession(t, testSessionTest{ + Inputs: []testSessionInput{ + { + Input: `[for i in [1, "two", true]: type(i)]`, + Error: true, + ErrorContains: "Invalid use of type function", + }, + }, + }) + }) } func testSession(t *testing.T, test testSessionTest) { @@ -221,6 +247,9 @@ func testSession(t *testing.T, test testSessionTest) { t.Fatalf("failed to create scope: %s", diags.Err()) } + // Ensure that any console-only functions are available + scope.ConsoleMode = true + // Build the session s := &Session{ Scope: scope,