From c616950317ce26e79c0cf08877d90fe67cc14e1f Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Fri, 16 Dec 2022 21:12:38 +0100 Subject: [PATCH] Pretty print PCL types This is part of an effort to make PCL type error messages better. --- pkg/codegen/hcl2/model/diagnostics.go | 8 +- pkg/codegen/hcl2/model/pretty/display.go | 337 ++++++++++++++++++ pkg/codegen/hcl2/model/pretty/display_test.go | 198 ++++++++++ pkg/codegen/hcl2/model/type.go | 6 +- pkg/codegen/hcl2/model/type_const.go | 6 + pkg/codegen/hcl2/model/type_enum.go | 22 +- pkg/codegen/hcl2/model/type_list.go | 10 + pkg/codegen/hcl2/model/type_map.go | 10 + pkg/codegen/hcl2/model/type_none.go | 6 + pkg/codegen/hcl2/model/type_object.go | 14 +- pkg/codegen/hcl2/model/type_opaque.go | 5 + pkg/codegen/hcl2/model/type_output.go | 9 + pkg/codegen/hcl2/model/type_promise.go | 10 + pkg/codegen/hcl2/model/type_set.go | 9 + pkg/codegen/hcl2/model/type_tuple.go | 20 +- pkg/codegen/hcl2/model/type_union.go | 25 ++ 16 files changed, 685 insertions(+), 10 deletions(-) create mode 100644 pkg/codegen/hcl2/model/pretty/display.go create mode 100644 pkg/codegen/hcl2/model/pretty/display_test.go diff --git a/pkg/codegen/hcl2/model/diagnostics.go b/pkg/codegen/hcl2/model/diagnostics.go index b63327450978..b03c9ab790b3 100644 --- a/pkg/codegen/hcl2/model/diagnostics.go +++ b/pkg/codegen/hcl2/model/diagnostics.go @@ -44,13 +44,13 @@ func ExprNotConvertible(destType Type, expr Expression) *hcl.Diagnostic { if len(why) != 0 { return errorf(expr.SyntaxNode().Range(), why[0].Summary) } - return errorf(expr.SyntaxNode().Range(), "cannot assign expression of type %v to location of type %v: ", expr.Type(), - destType) + return errorf(expr.SyntaxNode().Range(), "cannot assign expression of type %s to location of type %s: ", expr.Type().Pretty(), + destType.Pretty()) } func typeNotConvertible(dest, src Type) *hcl.Diagnostic { - return &hcl.Diagnostic{Severity: hcl.DiagError, Summary: fmt.Sprintf("cannot assign value of type %v to type %v", - src, dest)} + return &hcl.Diagnostic{Severity: hcl.DiagError, Summary: fmt.Sprintf("cannot assign value of type %s to type %s", + src.Pretty(), dest.Pretty())} } func tuplesHaveDifferentLengths(dest, src *TupleType) *hcl.Diagnostic { diff --git a/pkg/codegen/hcl2/model/pretty/display.go b/pkg/codegen/hcl2/model/pretty/display.go new file mode 100644 index 000000000000..4267c122275a --- /dev/null +++ b/pkg/codegen/hcl2/model/pretty/display.go @@ -0,0 +1,337 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// pretty is an extensible utility library to pretty-print nested structures. +package pretty + +import ( + "fmt" + "sort" + "strings" +) + +const ( + DefaultColumns int = 100 + DefaultIndent string = " " +) + +// A formatter understands how to turn itself into a string while respecting a desired +// column target. +type Formatter interface { + fmt.Stringer + + // Set the number of columns to print out. + // This method does not mutate. + Columns(int) Formatter +} + +// Indent a (multi-line) string, passing on column adjustments. +type indent struct { + // The prefix to be applied to each line + prefix string + inner Formatter +} + +func sanatizeColumns(i int) int { + if i <= 0 { + return 1 + } + return i +} + +func (i indent) Columns(columns int) Formatter { + i.inner = i.inner.Columns(columns - len(i.prefix)) + return i +} + +func (i indent) String() string { + lines := strings.Split(i.inner.String(), "\n") + for j, l := range lines { + lines[j] = i.prefix + l + } + return strings.Join(lines, "\n") +} + +// Create a new Formatter of a raw string. +func FromString(s string) Formatter { + return &literal{s: s} +} + +// Create a new Formatter from a fmt.Stringer. +func FromStringer(s fmt.Stringer) Formatter { + return &literal{t: s} +} + +// A string literal that implements Formatter (ignoring Column). +type literal struct { + // A source for a string. + t fmt.Stringer + // A raw string. + s string +} + +func (b *literal) String() string { + // If we don't have a cached value, but we can compute one, + if b.s == "" && b.t != nil { + // Set the known value to the computed value + b.s = b.t.String() + // Nil the value source, since we won't need it again, and it might produce "". + b.t = nil + } + return b.s +} + +func (b literal) Columns(int) Formatter { + // We are just calling .String() here, so we can't do anything with columns. + return &b +} + +// A Formatter that wraps an inner value with prefixes and postfixes. +// +// Wrap attempts to respect its column target by changing if the prefix and postfix are on +// the same line as the inner value, or their own lines. +// +// As an example, consider the following instance of Wrap: +// +// Wrap { +// Prefix: "number(", Postfix: ")" +// Value: FromString("123456") +// } +// +// It could be rendered as +// +// number(123456) +// +// or +// +// number( +// 123456 +// ) +// +// depending on the column constrains. +type Wrap struct { + Prefix, Postfix string + // Require that the Postfix is always on the same line as Value. + PostfixSameline bool + Value Formatter + + columns int +} + +func (w Wrap) String() string { + columns := w.columns + if columns == 0 { + columns = DefaultColumns + } + inner := w.Value.Columns(columns - len(w.Prefix) - len(w.Postfix)).String() + lines := strings.Split(inner, "\n") + + if len(lines) == 1 { + // Full result on one line, and it fits + if len(inner)+len(w.Prefix)+len(w.Postfix) < columns || + // Or its more efficient to include the wrapping instead of the indent. + len(w.Prefix)+len(w.Postfix) < len(DefaultIndent) { + return w.Prefix + inner + w.Postfix + } + + // Print the prefix and postix on their own line, then indent the inner value. + pre := w.Prefix + if pre != "" { + pre += "\n" + } + post := w.Postfix + if post != "" && !w.PostfixSameline { + post = "\n" + post + } + if w.PostfixSameline { + columns -= len(w.Postfix) + } + return pre + indent{ + prefix: DefaultIndent, + inner: w.Value, + }.Columns(columns).String() + post + } + + // See if we can afford to wrap the prefix & postfix around the first and last lines. + seperate := (w.Prefix != "" && len(w.Prefix)+len(lines[0]) >= columns) || + (w.Postfix != "" && len(w.Postfix)+len(lines[len(lines)-1]) >= columns && !w.PostfixSameline) + + if !seperate { + return w.Prefix + w.Value.Columns(columns).String() + w.Postfix + } + s := w.Prefix + if w.Prefix != "" { + s += "\n" + } + s += indent{ + prefix: DefaultIndent, + inner: w.Value, + }.Columns(columns).String() + if w.Postfix != "" && !w.PostfixSameline { + s += "\n" + w.Postfix + } + return s +} + +func (w Wrap) Columns(columns int) Formatter { + w.columns = sanatizeColumns(columns) + return w +} + +// Object is a Formatter that prints string-Formatter pairs, respecting columns where +// possible. +// +// It does this by deciding if the object should be compressed into a single line, or have +// one field per line. +type Object struct { + Properties map[string]Formatter + columns int +} + +func (o Object) String() string { + if len(o.Properties) == 0 { + return "{}" + } + columns := o.columns + if columns <= 0 { + columns = DefaultColumns + } + + // Check if we can do the whole object in a single line + keys := make([]string, 0, len(o.Properties)) + for key := range o.Properties { + keys = append(keys, key) + } + sort.StringSlice(keys).Sort() + + // Try to build the object in a single line + singleLine := true + s := "{ " + overflowing := func() bool { + return columns < len(s)-1 + } + for i, key := range keys { + s += key + ": " + v := o.Properties[key].Columns(columns - len(s) - 1).String() + if strings.IndexRune(v, '\n') != -1 { + singleLine = false + break + } + if i+1 < len(keys) { + v += "," + } + s += v + " " + + if overflowing() { + // The object is too big for a single line. Give up and create a multi-line + // object. + singleLine = false + break + } + } + if singleLine { + return s + "}" + } + + // reset for a mutl-line object. + s = "{\n" + for _, key := range keys { + s += indent{ + prefix: DefaultIndent, + inner: Wrap{ + Prefix: key + ": ", + Postfix: ",", + PostfixSameline: true, + Value: o.Properties[key], + }, + }.Columns(columns).String() + "\n" + } + return s + "}" +} + +func (o Object) Columns(columns int) Formatter { + o.columns = sanatizeColumns(columns) + return o +} + +// An ordered set of items displayed with a separator between them. +// +// Items can be displayed on a single line if it fits within the column constraint. +// Otherwise items will be displayed across multiple lines. +type List struct { + Elements []Formatter + Separator string + AdjoinSeparator bool + + columns int +} + +func (l List) String() string { + columns := l.columns + if columns <= 0 { + columns = DefaultColumns + } + s := "" + singleLine := true + for i, el := range l.Elements { + v := el.Columns(columns - len(s)).String() + if strings.IndexRune(v, '\n') != -1 { + singleLine = false + break + } + s += v + if i+1 < len(l.Elements) { + s += l.Separator + } + + if len(s) > columns { + singleLine = false + break + } + } + if singleLine { + return s + } + s = "" + if l.AdjoinSeparator { + seperator := strings.TrimRight(l.Separator, " ") + for i, el := range l.Elements { + v := el.Columns(columns - len(seperator)).String() + if i+1 != len(l.Elements) { + v += seperator + "\n" + } + s += v + } + return s + } + + seperator := strings.TrimLeft(l.Separator, " ") + for i, el := range l.Elements { + v := indent{ + prefix: strings.Repeat(" ", len(seperator)), + inner: el, + }.Columns(columns).String() + if i != 0 { + v = "\n" + seperator + v[len(seperator):] + } + s += v + } + return s + +} + +func (l List) Columns(columns int) Formatter { + l.columns = sanatizeColumns(columns) + return l +} diff --git a/pkg/codegen/hcl2/model/pretty/display_test.go b/pkg/codegen/hcl2/model/pretty/display_test.go new file mode 100644 index 000000000000..49c1148b0c28 --- /dev/null +++ b/pkg/codegen/hcl2/model/pretty/display_test.go @@ -0,0 +1,198 @@ +// Copyright 2016-2022, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pretty + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type test struct { + expected string + formatter Formatter +} + +func testFormatter(t *testing.T, tests []test) { + for _, tt := range tests { + tt := tt + t.Run("", func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.expected, tt.formatter.String()) + }) + } +} + +func TestIndentFormatter(t *testing.T) { + t.Parallel() + testFormatter(t, []test{ + { + ">>123", + indent{ + prefix: ">>", + inner: FromString("123"), + }, + }, + { + ">>123\n>>456", + indent{ + prefix: ">>", + inner: FromString("123\n456"), + }, + }, + }) +} + +func TestObjectFormatter(t *testing.T) { + t.Parallel() + + testFormatter(t, []test{ + { + "{ fizz: abc, hello: world }", + Object{ + Properties: map[string]Formatter{ + "fizz": FromString("abc"), + "hello": FromString("world"), + }, + }, + }, + { + "{\n fizz: abc,\n hello: world,\n}", + Object{ + Properties: map[string]Formatter{ + "fizz": FromString("abc"), + "hello": FromString("world"), + }, + }.Columns(18), + }, + { + "{\n aFoo: bar?,\n bFizz: \n buzz,\n}", + Object{ + Properties: map[string]Formatter{ + "aFoo": Wrap{ + Postfix: "?", + PostfixSameline: true, + Value: FromString("bar"), + }, + "bFizz": FromString("buzz"), + }, + }.Columns(14), + }, + }) +} + +func TestWrapFormatter(t *testing.T) { + t.Parallel() + + testFormatter(t, []test{ + { + "A(123456)", + Wrap{ + Prefix: "A(", + Postfix: ")", + Value: FromString("123456"), + }.Columns(10), + }, + { + "B(\n 123456\n)", + Wrap{ + Prefix: "B(", + Postfix: ")", + Value: FromString("123456"), + }.Columns(9), + }, + { + "C({\n 123456\n 123456\n})", + Wrap{ + Prefix: "C(", + Postfix: ")", + Value: FromString("{\n 123456\n 123456\n}"), + }.Columns(6), + }, + { + "foo-bar:\n fizz-buzz,", + Wrap{ + Prefix: "foo-bar:", + Postfix: ",", + PostfixSameline: true, + Value: FromString("fizz-buzz"), + }.Columns(8), + }, + }) +} + +func TestListFormatter(t *testing.T) { + t.Parallel() + + commaList := List{ + AdjoinSeparator: true, + Separator: ", ", + Elements: []Formatter{ + FromString("a"), + FromString("b"), + FromString("c"), + FromString("d"), + }, + } + + barList := List{ + Separator: " | ", + Elements: []Formatter{ + FromString("a"), + FromString("b"), + FromString("c"), + FromString("d"), + }, + } + + testFormatter(t, []test{ + {"a, b, c, d", commaList}, + {"a,\nb,\nc,\nd", commaList.Columns(4)}, + {"a | b | c | d", barList}, + {" a\n| b\n| c\n| d", barList.Columns(4)}, + {` a +| b +| [ + 1, + 2, + 3, + 4 + ] +| c`, + List{ + Separator: " | ", + Elements: []Formatter{ + FromString("a"), + FromString("b"), + Wrap{ + Prefix: "[", + Postfix: "]", + Value: List{ + AdjoinSeparator: true, + Separator: ", ", + Elements: []Formatter{ + FromString("1"), + FromString("2"), + FromString("3"), + FromString("4"), + }, + }, + }, + FromString("c"), + }, + }.Columns(4), + }, + }) +} diff --git a/pkg/codegen/hcl2/model/type.go b/pkg/codegen/hcl2/model/type.go index dc1ae4438c57..7ac708f27ea9 100644 --- a/pkg/codegen/hcl2/model/type.go +++ b/pkg/codegen/hcl2/model/type.go @@ -15,7 +15,10 @@ package model import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) @@ -36,12 +39,13 @@ func (k ConversionKind) Exists() bool { // Type represents a datatype in the Pulumi Schema. Types created by this package are identical if they are // equal values. type Type interface { + fmt.Stringer Definition Equals(other Type) bool AssignableFrom(src Type) bool ConversionFrom(src Type) ConversionKind - String() string + Pretty() pretty.Formatter equals(other Type, seen map[Type]struct{}) bool conversionFrom(src Type, unifying bool, seen map[Type]struct{}) (ConversionKind, lazyDiagnostics) diff --git a/pkg/codegen/hcl2/model/type_const.go b/pkg/codegen/hcl2/model/type_const.go index b124a9f7a0a8..0ceb51fb9264 100644 --- a/pkg/codegen/hcl2/model/type_const.go +++ b/pkg/codegen/hcl2/model/type_const.go @@ -19,6 +19,8 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/zclconf/go-cty/cty" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" ) // ConstType represents a type that is a single constant value. @@ -34,6 +36,10 @@ func NewConstType(typ Type, value cty.Value) *ConstType { return &ConstType{Type: typ, Value: value} } +func (t *ConstType) Pretty() pretty.Formatter { + return pretty.FromStringer(t) +} + // SyntaxNode returns the syntax node for the type. This is always syntax.None. func (*ConstType) SyntaxNode() hclsyntax.Node { return syntax.None diff --git a/pkg/codegen/hcl2/model/type_enum.go b/pkg/codegen/hcl2/model/type_enum.go index 2f701b08125c..721a73309041 100644 --- a/pkg/codegen/hcl2/model/type_enum.go +++ b/pkg/codegen/hcl2/model/type_enum.go @@ -18,10 +18,11 @@ import ( "fmt" "strings" - "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) @@ -76,6 +77,23 @@ func (*EnumType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (t *EnumType) Pretty() pretty.Formatter { + types := make([]pretty.Formatter, len(t.Elements)) + for i, c := range t.Elements { + types[i] = pretty.FromStringer( + NewConstType(ctyTypeToType(c.Type(), false), c).Pretty(), + ) + } + return pretty.Wrap{ + Prefix: "enum(", + Postfix: ")", + Value: pretty.List{ + Separator: " | ", + Elements: types, + }, + } +} + // Traverse attempts to traverse the enum type with the given traverser. This always fails. func (t *EnumType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { return &*t, nil diff --git a/pkg/codegen/hcl2/model/type_list.go b/pkg/codegen/hcl2/model/type_list.go index fd66115f6cf0..e0241aa791db 100644 --- a/pkg/codegen/hcl2/model/type_list.go +++ b/pkg/codegen/hcl2/model/type_list.go @@ -19,6 +19,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -38,6 +40,14 @@ func (*ListType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (l *ListType) Pretty() pretty.Formatter { + return pretty.Wrap{ + Prefix: "list(", + Postfix: ")", + Value: l.ElementType.Pretty(), + } +} + // Traverse attempts to traverse the optional type with the given traverser. The result type of traverse(list(T)) // is T; the traversal fails if the traverser is not a number. func (t *ListType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { diff --git a/pkg/codegen/hcl2/model/type_map.go b/pkg/codegen/hcl2/model/type_map.go index 666814c33422..dd699a1f35cb 100644 --- a/pkg/codegen/hcl2/model/type_map.go +++ b/pkg/codegen/hcl2/model/type_map.go @@ -19,6 +19,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -33,6 +35,14 @@ func NewMapType(elementType Type) *MapType { return &MapType{ElementType: elementType} } +func (t *MapType) Pretty() pretty.Formatter { + return pretty.Wrap{ + Prefix: "map(", + Value: t.ElementType.Pretty(), + Postfix: ")", + } +} + // Traverse attempts to traverse the optional type with the given traverser. The result type of traverse(map(T)) // is T; the traversal fails if the traverser is not a string. func (t *MapType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { diff --git a/pkg/codegen/hcl2/model/type_none.go b/pkg/codegen/hcl2/model/type_none.go index 52997860b361..a5287c3bc8a1 100644 --- a/pkg/codegen/hcl2/model/type_none.go +++ b/pkg/codegen/hcl2/model/type_none.go @@ -17,6 +17,8 @@ package model import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -26,6 +28,10 @@ func (noneType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (noneType) Pretty() pretty.Formatter { + return pretty.FromStringer(NoneType) +} + func (noneType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { return NoneType, hcl.Diagnostics{unsupportedReceiverType(NoneType, traverser.SourceRange())} } diff --git a/pkg/codegen/hcl2/model/type_object.go b/pkg/codegen/hcl2/model/type_object.go index 5ddb0eb6bc3b..94b9e3c21537 100644 --- a/pkg/codegen/hcl2/model/type_object.go +++ b/pkg/codegen/hcl2/model/type_object.go @@ -21,10 +21,12 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) // ObjectType represents schematized maps from strings to particular types. @@ -48,6 +50,14 @@ func (*ObjectType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (t *ObjectType) Pretty() pretty.Formatter { + m := make(map[string]pretty.Formatter, len(t.Properties)) + for k, v := range t.Properties { + m[k] = v.Pretty() + } + return pretty.Object{Properties: m} +} + // Traverse attempts to traverse the optional type with the given traverser. The result type of // traverse(object({K_0 = T_0, ..., K_N = T_N})) is T_i if the traverser is the string literal K_i. If the traverser is // a string but not a literal, the result type is any. diff --git a/pkg/codegen/hcl2/model/type_opaque.go b/pkg/codegen/hcl2/model/type_opaque.go index c13db0d4bcf4..abd904d442f6 100644 --- a/pkg/codegen/hcl2/model/type_opaque.go +++ b/pkg/codegen/hcl2/model/type_opaque.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) @@ -160,6 +161,10 @@ func NewOpaqueType(name string) *OpaqueType { return &t } +func (t *OpaqueType) Pretty() pretty.Formatter { + return pretty.FromStringer(t) +} + func (t *OpaqueType) string(_ map[Type]struct{}) string { return t.String() } diff --git a/pkg/codegen/hcl2/model/type_output.go b/pkg/codegen/hcl2/model/type_output.go index 65abd77f3816..180140da0f8b 100644 --- a/pkg/codegen/hcl2/model/type_output.go +++ b/pkg/codegen/hcl2/model/type_output.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -39,6 +40,14 @@ func (*OutputType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (t *OutputType) Pretty() pretty.Formatter { + return pretty.Wrap{ + Prefix: "output(", + Value: t.ElementType.Pretty(), + Postfix: ")", + } +} + // Traverse attempts to traverse the output type with the given traverser. The result type of traverse(output(T)) // is output(traverse(T)). func (t *OutputType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { diff --git a/pkg/codegen/hcl2/model/type_promise.go b/pkg/codegen/hcl2/model/type_promise.go index 5b558b302f8f..ba49c63b3c62 100644 --- a/pkg/codegen/hcl2/model/type_promise.go +++ b/pkg/codegen/hcl2/model/type_promise.go @@ -19,6 +19,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -39,6 +41,14 @@ func (*PromiseType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (t *PromiseType) Pretty() pretty.Formatter { + return pretty.Wrap{ + Prefix: "promise(", + Value: t.ElementType.Pretty(), + Postfix: ")", + } +} + // Traverse attempts to traverse the promise type with the given traverser. The result type of traverse(promise(T)) // is promise(traverse(T)). func (t *PromiseType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { diff --git a/pkg/codegen/hcl2/model/type_set.go b/pkg/codegen/hcl2/model/type_set.go index 8b2180741be8..400871c5cbc5 100644 --- a/pkg/codegen/hcl2/model/type_set.go +++ b/pkg/codegen/hcl2/model/type_set.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -98,6 +99,14 @@ func (t *SetType) conversionFrom(src Type, unifying bool, seen map[Type]struct{} }) } +func (l *SetType) Pretty() pretty.Formatter { + return pretty.Wrap{ + Prefix: "set(", + Value: l.ElementType.Pretty(), + Postfix: ")", + } +} + func (t *SetType) String() string { return t.string(nil) } diff --git a/pkg/codegen/hcl2/model/type_tuple.go b/pkg/codegen/hcl2/model/type_tuple.go index f268ed88f136..835d7c4b5ed8 100644 --- a/pkg/codegen/hcl2/model/type_tuple.go +++ b/pkg/codegen/hcl2/model/type_tuple.go @@ -21,8 +21,10 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/zclconf/go-cty/cty" + + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) // TupleType represents values that are a sequence of independently-typed elements. @@ -39,6 +41,22 @@ func NewTupleType(elementTypes ...Type) Type { return &TupleType{ElementTypes: elementTypes} } +func (t *TupleType) Pretty() pretty.Formatter { + elements := make([]pretty.Formatter, len(t.ElementTypes)) + for i, el := range t.ElementTypes { + elements[i] = el.Pretty() + } + return pretty.Wrap{ + Prefix: "(", + Value: pretty.List{ + AdjoinSeparator: true, + Separator: ", ", + Elements: elements, + }, + Postfix: ")", + } +} + // SyntaxNode returns the syntax node for the type. This is always syntax.None. func (*TupleType) SyntaxNode() hclsyntax.Node { return syntax.None diff --git a/pkg/codegen/hcl2/model/type_union.go b/pkg/codegen/hcl2/model/type_union.go index e0c4f5720c20..b8346a34828b 100644 --- a/pkg/codegen/hcl2/model/type_union.go +++ b/pkg/codegen/hcl2/model/type_union.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/pretty" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" ) @@ -105,6 +106,30 @@ func (*UnionType) SyntaxNode() hclsyntax.Node { return syntax.None } +func (t *UnionType) Pretty() pretty.Formatter { + elements := make([]pretty.Formatter, 0, len(t.ElementTypes)) + isOptional := false + for _, el := range t.ElementTypes { + if el == NoneType { + isOptional = true + continue + } + elements = append(elements, el.Pretty()) + } + var v pretty.Formatter = pretty.List{ + Separator: " | ", + Elements: elements, + } + if isOptional { + v = pretty.Wrap{ + Value: v, + Postfix: "?", + PostfixSameline: true, + } + } + return v +} + // Traverse attempts to traverse the union type with the given traverser. This always fails. func (t *UnionType) Traverse(traverser hcl.Traverser) (Traversable, hcl.Diagnostics) { var types []Type