From cfc201786f178771879c55fba710f933afab3c23 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 14 Apr 2025 11:37:19 -0500 Subject: [PATCH 01/27] chore: bad default values should throw errors --- cli/clidisplay/resources.go | 6 ++- extract/parameter.go | 7 +++- go.mod | 2 +- go.sum | 4 +- types/convert.go | 73 +++++++++++++++++++++++++++++++++++++ types/enum.go | 5 ++- types/parameter.go | 40 ++++++++++++++------ 7 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 types/convert.go diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go index ac93025..aad72ba 100644 --- a/cli/clidisplay/resources.go +++ b/cli/clidisplay/resources.go @@ -77,9 +77,13 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc //} else { // strVal = value.GoString() //} + dp := p.DisplayName + if p.DisplayName == "" { + dp = p.Name + } tableWriter.AppendRow(table.Row{ - fmt.Sprintf("(%s) %s: %s\n%s", p.DisplayName, p.Name, p.Description, formatOptions(selections, p.Options)), + fmt.Sprintf("(%s) %s: %s\n%s", dp, p.Name, p.Description, formatOptions(selections, p.Options)), }) if hcl.Diagnostics(p.Diagnostics).HasErrors() { diff --git a/extract/parameter.go b/extract/parameter.go index 3851635..faae374 100644 --- a/extract/parameter.go +++ b/extract/parameter.go @@ -173,6 +173,12 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti } } + if !diags.HasErrors() { + // Only do this validation if the parameter is valid, as if some errors + // exist, then this is likely to fail be excess information. + diags = diags.Extend(p.Valid()) + } + usageDiags := ParameterUsageDiagnostics(p) if usageDiags.HasErrors() { p.FormType = provider.ParameterFormTypeError @@ -243,7 +249,6 @@ func ParameterValidationFromBlock(block *terraform.Block) (types.ParameterValida Min: nullableInteger(block, "min"), Max: nullableInteger(block, "max"), Monotonic: nullableString(block, "monotonic"), - Invalid: nullableBoolean(block, "invalid"), } return p, diags diff --git a/go.mod b/go.mod index 84a2d03..8a0ca87 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/aquasecurity/trivy v0.58.2 github.com/coder/guts v1.0.2-0.20250227211802-139809366a22 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0 + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0.0.20250414140516-f66adaca2adf github.com/coder/websocket v1.8.13 github.com/go-chi/chi v4.1.2+incompatible github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 68ca4f3..72803d2 100644 --- a/go.sum +++ b/go.sum @@ -718,8 +718,8 @@ github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0 h1:NPt2+FVr+2QJoxrta5ZwyTaxocWMEKdh2WpIumffxfM= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0.0.20250414140516-f66adaca2adf h1:h0ZMBLv/NiHeMWANSiUKHZkuxti4zIWCGXAXYF0tuJQ= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0.0.20250414140516-f66adaca2adf/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/types/convert.go b/types/convert.go new file mode 100644 index 0000000..f8ee948 --- /dev/null +++ b/types/convert.go @@ -0,0 +1,73 @@ +package types + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + + "github.com/coder/terraform-provider-coder/v2/provider" +) + +func providerValidations(vals []*ParameterValidation) []provider.Validation { + cpy := make([]provider.Validation, 0, len(vals)) + for _, val := range vals { + cpy = append(cpy, providerValidation(val)) + } + return cpy +} + +func providerValidation(v *ParameterValidation) provider.Validation { + return provider.Validation{ + Min: int(orZero(v.Min)), + MinDisabled: v.Min == nil, + Max: int(orZero(v.Max)), + MaxDisabled: v.Max == nil, + Monotonic: orZero(v.Monotonic), + Regex: orZero(v.Regex), + Error: v.Error, + } +} + +func providerOptions(opts []*ParameterOption) []provider.Option { + cpy := make([]provider.Option, 0, len(opts)) + for _, opt := range opts { + cpy = append(cpy, providerOption(opt)) + } + return cpy +} + +func providerOption(opt *ParameterOption) provider.Option { + return provider.Option{ + Name: opt.Name, + Description: opt.Description, + Value: opt.Value.AsString(), + Icon: opt.Icon, + } +} + +func hclDiagnostics(diagnostics diag.Diagnostics) hcl.Diagnostics { + cpy := make(hcl.Diagnostics, 0, len(diagnostics)) + for _, d := range diagnostics { + cpy = append(cpy, hclDiagnostic(d)) + } + return cpy +} + +func hclDiagnostic(d diag.Diagnostic) *hcl.Diagnostic { + sev := hcl.DiagInvalid + switch d.Severity { + case diag.Error: + sev = hcl.DiagError + case diag.Warning: + sev = hcl.DiagWarning + } + return &hcl.Diagnostic{ + Severity: sev, + Summary: d.Summary, + Detail: d.Detail, + Subject: nil, + Context: nil, + Expression: nil, + EvalContext: nil, + Extra: nil, + } +} diff --git a/types/enum.go b/types/enum.go index ab29945..190668b 100644 --- a/types/enum.go +++ b/types/enum.go @@ -3,9 +3,12 @@ package types import ( "fmt" "strings" + + "github.com/coder/terraform-provider-coder/v2/provider" ) -type ParameterType string +// TODO: Just use the provider type directly. +type ParameterType provider.OptionType const ( ParameterTypeString ParameterType = "string" diff --git a/types/parameter.go b/types/parameter.go index 6f58ff2..90f8c79 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/coder/terraform-provider-coder/v2/provider" @@ -73,24 +74,16 @@ type ParameterValidation struct { Min *int64 `json:"validation_min"` Max *int64 `json:"validation_max"` Monotonic *string `json:"validation_monotonic"` - Invalid *bool `json:"validation_invalid"` } // Valid takes the type of the value and the value itself and returns an error // if the value is invalid. -func (v ParameterValidation) Valid(typ string, value string) error { +func (v *ParameterValidation) Valid(typ string, value string) error { // TODO: Validate typ is the enum? // Use the provider.Validation struct to validate the value to be // consistent with the provider. - return (&provider.Validation{ - Min: int(orZero(v.Min)), - MinDisabled: v.Min == nil, - Max: int(orZero(v.Max)), - MaxDisabled: v.Max == nil, - Monotonic: orZero(v.Monotonic), - Regex: orZero(v.Regex), - Error: v.Error, - }).Valid(provider.OptionType(typ), value) + pv := providerValidation(v) + return (&pv).Valid(provider.OptionType(typ), value) } type ParameterOption struct { @@ -100,6 +93,31 @@ type ParameterOption struct { Icon string `json:"icon"` } +func (r *ParameterData) Valid() hcl.Diagnostics { + diag := (&provider.Parameter{ + Name: r.Name, + DisplayName: r.DisplayName, + Description: r.Description, + Type: provider.OptionType(r.Type), + FormType: r.FormType, + Mutable: r.Mutable, + Default: r.DefaultValue.AsString(), + Icon: r.Icon, + Option: providerOptions(r.Options), + Validation: providerValidations(r.Validations), + Optional: false, + Order: int(r.Order), + Ephemeral: r.Ephemeral, + }).Valid() + + if diag.HasError() { + // TODO: We can take the attr path and decorate the error with + // source information. + return hclDiagnostics(diag) + } + return nil +} + // CtyType returns the cty.Type for the ParameterData. // A fixed set of types are supported. func (r *ParameterData) CtyType() (cty.Type, error) { From db2467948150ac3882b685334c962b0434be1b0c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 09:55:12 -0500 Subject: [PATCH 02/27] add more error context --- types/convert.go | 30 ++++++++++++++++++++++++++---- types/parameter.go | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/types/convert.go b/types/convert.go index f8ee948..354db93 100644 --- a/types/convert.go +++ b/types/convert.go @@ -1,8 +1,15 @@ package types import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/iac/terraform" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/zclconf/go-cty/cty" + hcty "github.com/hashicorp/go-cty/cty" + hctyjson "github.com/hashicorp/go-cty/cty/json" + ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/coder/terraform-provider-coder/v2/provider" ) @@ -44,15 +51,15 @@ func providerOption(opt *ParameterOption) provider.Option { } } -func hclDiagnostics(diagnostics diag.Diagnostics) hcl.Diagnostics { +func hclDiagnostics(diagnostics diag.Diagnostics, source *terraform.Block) hcl.Diagnostics { cpy := make(hcl.Diagnostics, 0, len(diagnostics)) for _, d := range diagnostics { - cpy = append(cpy, hclDiagnostic(d)) + cpy = append(cpy, hclDiagnostic(d, source)) } return cpy } -func hclDiagnostic(d diag.Diagnostic) *hcl.Diagnostic { +func hclDiagnostic(d diag.Diagnostic, source *terraform.Block) *hcl.Diagnostic { sev := hcl.DiagInvalid switch d.Severity { case diag.Error: @@ -60,11 +67,26 @@ func hclDiagnostic(d diag.Diagnostic) *hcl.Diagnostic { case diag.Warning: sev = hcl.DiagWarning } + + // This is an imperfect way to finding the source code of the error. There is 2 + // different `cty` types at place here, the hashicorp fork and the original. So a + // more general solution is difficult. This is good enough for now to add more + // context to an error. + var subject *hcl.Range + if len(d.AttributePath) == 1 && source != nil { + if attr, ok := d.AttributePath[0].(hcty.GetAttrStep); ok { + src := source.GetAttribute(attr.Name) + if src != nil { + subject = &(src.HCLAttribute().Range) + } + } + } + return &hcl.Diagnostic{ Severity: sev, Summary: d.Summary, Detail: d.Detail, - Subject: nil, + Subject: subject, Context: nil, Expression: nil, EvalContext: nil, diff --git a/types/parameter.go b/types/parameter.go index 90f8c79..ffa0504 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -113,7 +113,7 @@ func (r *ParameterData) Valid() hcl.Diagnostics { if diag.HasError() { // TODO: We can take the attr path and decorate the error with // source information. - return hclDiagnostics(diag) + return hclDiagnostics(diag, source) } return nil } From ed766d08f9cd3056fda489494df8051b91f0a3fa Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 09:55:33 -0500 Subject: [PATCH 03/27] add more error context --- types/convert.go | 7 +------ types/parameter.go | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/types/convert.go b/types/convert.go index 354db93..3d395e5 100644 --- a/types/convert.go +++ b/types/convert.go @@ -1,15 +1,10 @@ package types import ( - "fmt" - "github.com/aquasecurity/trivy/pkg/iac/terraform" + hcty "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/zclconf/go-cty/cty" - hcty "github.com/hashicorp/go-cty/cty" - hctyjson "github.com/hashicorp/go-cty/cty/json" - ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/coder/terraform-provider-coder/v2/provider" ) diff --git a/types/parameter.go b/types/parameter.go index ffa0504..cf2565b 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -113,7 +113,7 @@ func (r *ParameterData) Valid() hcl.Diagnostics { if diag.HasError() { // TODO: We can take the attr path and decorate the error with // source information. - return hclDiagnostics(diag, source) + return hclDiagnostics(diag, r.Source) } return nil } From 3fcc847522f0f7c402fc84bb52bf1f0dd545caf7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 10:40:53 -0500 Subject: [PATCH 04/27] terraform provider dep update --- go.mod | 6 +++--- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 7ea2251..ea18140 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,16 @@ require ( github.com/aquasecurity/trivy v0.58.2 github.com/coder/guts v1.0.2-0.20250227211802-139809366a22 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817 github.com/coder/websocket v1.8.13 github.com/go-chi/chi v4.1.2+incompatible + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/terraform-exec v0.23.0 github.com/hashicorp/terraform-json v0.24.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/stretchr/testify v1.10.0 github.com/zclconf/go-cty v1.16.2 @@ -69,7 +71,6 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -79,7 +80,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/go.sum b/go.sum index c22d751..81daf7e 100644 --- a/go.sum +++ b/go.sum @@ -718,8 +718,8 @@ github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817 h1:4ryzTbgCe+AHk/G03y48Rtg3Y+pfr5FfhGt/eEckJ6g= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= From 1a0b055a14938448e34705a5625e454ecf90af7b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 15:36:58 -0500 Subject: [PATCH 05/27] accept empty default values --- extract/parameter.go | 51 +++++++++-------------------------- go.mod | 2 +- go.sum | 4 +-- preview_test.go | 10 +++++++ testdata/emptydefault/main.tf | 26 ++++++++++++++++++ types/parameter.go | 21 ++++++++++++--- types/value.go | 8 ++++++ 7 files changed, 77 insertions(+), 45 deletions(-) create mode 100644 testdata/emptydefault/main.tf diff --git a/extract/parameter.go b/extract/parameter.go index faae374..b5b3b05 100644 --- a/extract/parameter.go +++ b/extract/parameter.go @@ -50,7 +50,7 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti pVal := richParameterValue(block) - def := types.StringLiteral("") + def := types.NullString() defAttr := block.GetAttribute("default") if !defAttr.IsNil() { def = types.ToHCLString(block, defAttr) @@ -138,45 +138,10 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti p.Validations = append(p.Validations, &valid) } - ctyType, err := p.CtyType() - if err != nil { - paramTypeDiag := &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Invalid parameter type %q", p.Type), - Detail: err.Error(), - Context: &block.HCLBlock().DefRange, - } - - if attr := block.GetAttribute("type"); attr != nil && !attr.IsNil() { - paramTypeDiag.Subject = &attr.HCLAttribute().Range - paramTypeDiag.Expression = attr.HCLAttribute().Expr - paramTypeDiag.EvalContext = block.Context().Inner() - } - diags = diags.Append(paramTypeDiag) - p.FormType = provider.ParameterFormTypeError - } - - if ctyType != cty.NilType && pVal.Value.Type().Equals(cty.String) { - // TODO: Wish we could support more types, but only string types are - // allowed. - valStr := pVal.Value.AsString() - // Apply validations to the parameter value - for _, v := range p.Validations { - if err := v.Valid(string(pType), valStr); err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Paramater validation failed for value %q", valStr), - Detail: err.Error(), - Expression: pVal.ValueExpr, - }) - } - } - } - if !diags.HasErrors() { // Only do this validation if the parameter is valid, as if some errors // exist, then this is likely to fail be excess information. - diags = diags.Extend(p.Valid()) + diags = diags.Extend(p.Valid(p.Value)) } usageDiags := ParameterUsageDiagnostics(p) @@ -194,7 +159,9 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti func ParameterUsageDiagnostics(p types.Parameter) hcl.Diagnostics { valErr := "The value of a parameter is required to be sourced (default or input) for the parameter to function." var diags hcl.Diagnostics - if !p.Value.Valid() { + if p.Value.Value.IsNull() { + // Allow null values + } else if !p.Value.Valid() { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Parameter value is not valid", @@ -478,6 +445,14 @@ func richParameterValue(block *terraform.Block) types.HCLString { val, diags := valRef.Value(block.Context().Inner()) source := hclext.CreateDotReferenceFromTraversal(valRef.Traversal) + + // If no value attribute exists, then the value is `null`. + if diags.HasErrors() && diags[0].Summary == "Unsupported attribute" { + s := types.NullString() + s.Source = &source + return s + } + return types.HCLString{ Value: val, ValueDiags: diags, diff --git a/go.mod b/go.mod index ea18140..f332927 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aquasecurity/trivy v0.58.2 github.com/coder/guts v1.0.2-0.20250227211802-139809366a22 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817 + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250505161541-0fd96eeace73 github.com/coder/websocket v1.8.13 github.com/go-chi/chi v4.1.2+incompatible github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 diff --git a/go.sum b/go.sum index 81daf7e..e797eda 100644 --- a/go.sum +++ b/go.sum @@ -718,8 +718,8 @@ github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817 h1:4ryzTbgCe+AHk/G03y48Rtg3Y+pfr5FfhGt/eEckJ6g= -github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250502142605-22f12cb73817/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250505161541-0fd96eeace73 h1:Gax/pSsln9cSTueP5teoWM4EPqEux4BUp7VlECiuW2M= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250505161541-0fd96eeace73/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/preview_test.go b/preview_test.go index 0c91d36..f0061e3 100644 --- a/preview_test.go +++ b/preview_test.go @@ -211,6 +211,16 @@ func Test_Extract(t *testing.T) { unknownTags: []string{}, params: map[string]assertParam{}, }, + { + name: "empty default", + dir: "emptydefault", + expTags: map[string]string{}, + input: preview.Input{}, + unknownTags: []string{}, + params: map[string]assertParam{ + "word": ap(), + }, + }, { name: "many modules", dir: "manymodules", diff --git a/testdata/emptydefault/main.tf b/testdata/emptydefault/main.tf new file mode 100644 index 0000000..baa398e --- /dev/null +++ b/testdata/emptydefault/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + } +} + +data "coder_parameter" "word" { + name = "word" + description = "Select something" + type = "string" + order = 1 + # No default selected + + option { + name = "Bird" + value = "bird" + description = "An animal that can fly." + } + option { + name = "Boat" + value = "boat" + } +} diff --git a/types/parameter.go b/types/parameter.go index cf2565b..ebb8988 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -93,22 +93,35 @@ type ParameterOption struct { Icon string `json:"icon"` } -func (r *ParameterData) Valid() hcl.Diagnostics { - diag := (&provider.Parameter{ +func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics { + var defPtr *string + if !r.DefaultValue.Value.IsNull() { + def := r.DefaultValue.Value.AsString() + defPtr = &def + } + + var valuePtr *string + // TODO: What to do if it is not valid? + if value.Valid() { + val := value.Value.AsString() + valuePtr = &val + } + + _, diag := (&provider.Parameter{ Name: r.Name, DisplayName: r.DisplayName, Description: r.Description, Type: provider.OptionType(r.Type), FormType: r.FormType, Mutable: r.Mutable, - Default: r.DefaultValue.AsString(), + Default: defPtr, Icon: r.Icon, Option: providerOptions(r.Options), Validation: providerValidations(r.Validations), Optional: false, Order: int(r.Order), Ephemeral: r.Ephemeral, - }).Valid() + }).ValidateInput(valuePtr) if diag.HasError() { // TODO: We can take the attr path and decorate the error with diff --git a/types/value.go b/types/value.go index 4cc269b..4e22b91 100644 --- a/types/value.go +++ b/types/value.go @@ -73,6 +73,14 @@ func StringLiteral(s string) HCLString { } } +func NullString() HCLString { + v := cty.NullVal(cty.String) + return HCLString{ + Value: v, + ValueExpr: &hclsyntax.LiteralValueExpr{Val: v}, + } +} + // AsString is a safe function. It will always return a string. // The caller should check if this value is Valid and known before // calling this function. From 4f4463b504ea8be125d3e70ed3246ed4d404aa9e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 15:40:27 -0500 Subject: [PATCH 06/27] populate required field --- extract/parameter.go | 4 +++- extract/state.go | 2 +- types/parameter.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extract/parameter.go b/extract/parameter.go index b5b3b05..86b09f6 100644 --- a/extract/parameter.go +++ b/extract/parameter.go @@ -50,10 +50,12 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti pVal := richParameterValue(block) + requiredValue := true def := types.NullString() defAttr := block.GetAttribute("default") if !defAttr.IsNil() { def = types.ToHCLString(block, defAttr) + requiredValue = false } ftmeta := optionalString(block, "styling") @@ -77,7 +79,7 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti Icon: optionalString(block, "icon"), Options: make([]*types.ParameterOption, 0), Validations: make([]*types.ParameterValidation, 0), - Required: optionalBoolean(block, "required"), + Required: requiredValue, DisplayName: optionalString(block, "display_name"), Order: optionalInteger(block, "order"), Ephemeral: optionalBoolean(block, "ephemeral"), diff --git a/extract/state.go b/extract/state.go index bb1a942..905cf6a 100644 --- a/extract/state.go +++ b/extract/state.go @@ -74,7 +74,7 @@ func ParameterFromState(block *tfjson.StateResource) (types.Parameter, error) { Icon: st.optionalString("icon"), Options: options, Validations: validations, - Required: st.optionalBool("required"), + Required: !st.optionalBool("optional"), DisplayName: st.optionalString("display_name"), Order: st.optionalInteger("order"), Ephemeral: st.optionalBool("ephemeral"), diff --git a/types/parameter.go b/types/parameter.go index ebb8988..a155b03 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -118,7 +118,7 @@ func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics { Icon: r.Icon, Option: providerOptions(r.Options), Validation: providerValidations(r.Validations), - Optional: false, + Optional: !r.Required, Order: int(r.Order), Ephemeral: r.Ephemeral, }).ValidateInput(valuePtr) From dd15cc786002df7c92b23718d50b1179441073cb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 15:44:00 -0500 Subject: [PATCH 07/27] make gen --- site/src/types/preview.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/types/preview.ts b/site/src/types/preview.ts index 945c7e1..1ab72e5 100644 --- a/site/src/types/preview.ts +++ b/site/src/types/preview.ts @@ -67,7 +67,6 @@ export interface ParameterValidation { readonly validation_min: number | null; readonly validation_max: number | null; readonly validation_monotonic: string | null; - readonly validation_invalid: boolean | null; } // From web/session.go From 5104c529d6309b55a724dbc780097420ea8ba5ed Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 15:48:10 -0500 Subject: [PATCH 08/27] fixup default usage --- types/parameter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/parameter.go b/types/parameter.go index a155b03..700b42e 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -95,7 +95,8 @@ type ParameterOption struct { func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics { var defPtr *string - if !r.DefaultValue.Value.IsNull() { + + if r.DefaultValue.Valid() && r.DefaultValue.IsKnown() { def := r.DefaultValue.Value.AsString() defPtr = &def } From 3ba59d2fb72acdc5f5b330489b21c800f445ddc7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 16:05:08 -0500 Subject: [PATCH 09/27] fix AsString reference --- cli/clidisplay/resources.go | 2 +- types/parameter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go index aad72ba..dea4ba9 100644 --- a/cli/clidisplay/resources.go +++ b/cli/clidisplay/resources.go @@ -30,7 +30,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { k, v := tag.AsStrings() tableWriter.AppendRow(table.Row{k, v, ""}) continue - //diags = diags.Extend(tDiags) + // diags = diags.Extend(tDiags) //if !diags.HasErrors() { // tableWriter.AppendRow(table.Row{k, v, ""}) // continue diff --git a/types/parameter.go b/types/parameter.go index 700b42e..6eeee34 100644 --- a/types/parameter.go +++ b/types/parameter.go @@ -97,7 +97,7 @@ func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics { var defPtr *string if r.DefaultValue.Valid() && r.DefaultValue.IsKnown() { - def := r.DefaultValue.Value.AsString() + def := r.DefaultValue.AsString() defPtr = &def } From b198910aa54d62c894038a18ff32c9b517ed9289 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 16:06:06 -0500 Subject: [PATCH 10/27] chore: add golangci-lint --- .golangci.yaml | 269 ++++++++++++++++++++++++++++++++++++ cli/clidisplay/resources.go | 8 +- cmd/preview/main.go | 3 +- log.go | 2 +- paramhook.go | 2 +- plan.go | 2 +- preview.go | 4 +- previewe2e_test.go | 4 +- scripts/rules.go | 16 +++ workspacetags.go | 12 +- 10 files changed, 304 insertions(+), 18 deletions(-) create mode 100644 .golangci.yaml create mode 100644 scripts/rules.go diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..df8f68a --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,269 @@ +# See https://golangci-lint.run/usage/configuration/ +# Over time we should try tightening some of these. + +linters-settings: + dupl: + # goal: 100 + threshold: 412 + + exhaustruct: + include: + # Gradually extend to cover more of the codebase. + - 'httpmw\.\w+' + # We want to enforce all values are specified when inserting or updating + # a database row. Ref: #9936 + - 'github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params' + gocognit: + min-complexity: 300 + + goconst: + min-len: 4 # Min length of string consts (def 3). + min-occurrences: 3 # Min number of const occurrences (def 3). + + gocritic: + enabled-checks: + # - appendAssign + # - appendCombine + # - assignOp + # - badCall + - badLock + - badRegexp + - boolExprSimplify + # - builtinShadow + - builtinShadowDecl + # - commentedOutCode + - commentedOutImport + - deferUnlambda + # - deprecatedComment + # - docStub + - dupImport + # - elseif + - emptyFallthrough + # - emptyStringTest + # - equalFold + # - evalOrder + # - exitAfterDefer + # - exposedSyncMutex + # - filepathJoin + - hexLiteral + # - httpNoBody + # - hugeParam + # - ifElseChain + # - importShadow + - indexAlloc + - initClause + - methodExprCall + # - nestingReduce + - nilValReturn + # - octalLiteral + # - paramTypeCombine + # - preferStringWriter + # - preferWriteByte + # - ptrToRefParam + # - rangeExprCopy + # - rangeValCopy + - regexpPattern + # - regexpSimplify + - ruleguard + # - sloppyReassign + - sortSlice + - sprintfQuotedString + - sqlQuery + # - stringConcatSimplify + # - stringXbytes + # - suspiciousSorting + - truncateCmp + - typeAssertChain + # - typeDefFirst + # - typeUnparen + # - unlabelStmt + # - unlambda + # - unnamedResult + # - unnecessaryBlock + # - unnecessaryDefer + # - unslice + - weakCond + # - whyNoLint + # - wrapperFunc + # - yodaStyleExpr + settings: + ruleguard: + failOn: all + rules: "${configDir}/scripts/rules.go" + + staticcheck: + # https://staticcheck.io/docs/options#checks + # We disable SA1019 because it gets angry about our usage of xerrors. We + # intentionally xerrors because stack frame support didn't make it into the + # stdlib port. + checks: ["all", "-SA1019"] + + goimports: + local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder + + importas: + no-unaliased: true + + misspell: + locale: US + ignore-words: + - trialer + + nestif: + # goal: 10 + min-complexity: 20 + + revive: + # see https://github.com/mgechev/revive#available-rules for details. + ignore-generated-header: true + severity: warning + rules: + - name: atomic + - name: bare-return + - name: blank-imports + - name: bool-literal-in-expr + - name: call-to-gc + - name: confusing-naming + - name: confusing-results + - name: constant-logical-expr + - name: context-as-argument + - name: context-keys-type + - name: deep-exit + - name: defer + - name: dot-imports + - name: duplicated-imports + - name: early-return + - name: empty-block + - name: empty-lines + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: exported + - name: flag-parameter + - name: get-return + - name: identical-branches + - name: if-return + - name: import-shadowing + - name: increment-decrement + - name: indent-error-flow + # - name: modifies-parameter + - name: modifies-value-receiver + - name: package-comments + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: struct-tag + - name: superfluous-else + - name: time-naming + - name: unconditional-recursion + - name: unexported-naming + - name: unexported-return + - name: unhandled-error + - name: unnecessary-stmt + - name: unreachable-code + - name: unused-parameter + exclude: "**/*_test.go" + - name: unused-receiver + - name: var-declaration + - name: var-naming + - name: waitgroup-by-value + + # irrelevant as of Go v1.22: https://go.dev/blog/loopvar-preview + govet: + disable: + - loopclosure + gosec: + excludes: + # Implicit memory aliasing of items from a range statement (irrelevant as of Go v1.22) + - G601 + +issues: + exclude-dirs: + - coderd/database/dbmem + - node_modules + - .git + + exclude-files: + - scripts/rules.go + + # Rules listed here: https://github.com/securego/gosec#available-rules + exclude-rules: + - path: _test\.go + linters: + # We use assertions rather than explicitly checking errors in tests + - errcheck + - forcetypeassert + - exhaustruct # This is unhelpful in tests. + - path: scripts/* + linters: + - exhaustruct + - path: scripts/rules.go + linters: + - ALL + + fix: true + max-issues-per-linter: 0 + max-same-issues: 0 + +run: + timeout: 10m + +# Over time, add more and more linters from +# https://golangci-lint.run/usage/linters/ as the code improves. +linters: + disable-all: true + enable: + - asciicheck + - bidichk + - bodyclose + - dogsled + - errcheck + - errname + - errorlint + - exhaustruct + - forcetypeassert + - gocritic + # gocyclo is may be useful in the future when we start caring + # about testing complexity, but for the time being we should + # create a good culture around cognitive complexity. + # - gocyclo + - gocognit + - nestif + - goimports + - gomodguard + - gosec + - gosimple + - govet + - importas + - ineffassign + - makezero + - misspell + - nilnil + - noctx + - paralleltest + - revive + + # These don't work until the following issue is solved. + # https://github.com/golangci/golangci-lint/issues/2649 + # - rowserrcheck + # - sqlclosecheck + # - structcheck + # - wastedassign + + - staticcheck + - tenv + # In Go, it's possible for a package to test it's internal functionality + # without testing any exported functions. This is enabled to promote + # decomposing a package before testing it's internals. A function caller + # should be able to test most of the functionality from exported functions. + # + # There are edge-cases to this rule, but they should be carefully considered + # to avoid structural inconsistency. + - testpackage + - tparallel + - typecheck + - unconvert + - unused + - dupl \ No newline at end of file diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go index ac93025..ec2ca94 100644 --- a/cli/clidisplay/resources.go +++ b/cli/clidisplay/resources.go @@ -30,7 +30,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { k, v := tag.AsStrings() tableWriter.AppendRow(table.Row{k, v, ""}) continue - //diags = diags.Extend(tDiags) + // diags = diags.Extend(tDiags) //if !diags.HasErrors() { // tableWriter.AppendRow(table.Row{k, v, ""}) // continue @@ -41,7 +41,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { refs := tag.References() tableWriter.AppendRow(table.Row{k, "??", strings.Join(refs, "\n")}) - //refs := tb.AllReferences() + // refs := tb.AllReferences() //refsStr := make([]string, 0, len(refs)) //for _, ref := range refs { // refsStr = append(refsStr, ref.String()) @@ -55,7 +55,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hcl.File) { tableWriter := table.NewWriter() - //tableWriter.SetTitle("Parameters") + // tableWriter.SetTitle("Parameters") tableWriter.SetStyle(table.StyleLight) tableWriter.Style().Options.SeparateColumns = false row := table.Row{"Parameter"} @@ -66,7 +66,7 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc if p.FormType == provider.ParameterFormTypeMultiSelect { _ = json.Unmarshal([]byte(strVal), &selections) } - //value := p.Value.Value + // value := p.Value.Value // //if value.IsNull() { // strVal = "null" diff --git a/cmd/preview/main.go b/cmd/preview/main.go index a035d2c..4adb498 100644 --- a/cmd/preview/main.go +++ b/cmd/preview/main.go @@ -5,8 +5,9 @@ import ( "log" "os" - "github.com/coder/preview/cli" "github.com/hashicorp/hcl/v2" + + "github.com/coder/preview/cli" ) func main() { diff --git a/log.go b/log.go index db34405..ed1c223 100644 --- a/log.go +++ b/log.go @@ -11,5 +11,5 @@ func init() { Level: tlog.LevelDebug, })) var _ = ll - //tlog.SetDefault(ll) + // tlog.SetDefault(ll) } diff --git a/paramhook.go b/paramhook.go index ec10a33..b8c0cd4 100644 --- a/paramhook.go +++ b/paramhook.go @@ -156,7 +156,7 @@ func isForEachKey(key cty.Value) bool { func evaluateCoderParameterDefault(b *terraform.Block) (cty.Value, bool) { attributes := b.Attributes() - //typeAttr, exists := attributes["type"] + // typeAttr, exists := attributes["type"] //valueType := cty.String // TODO: Default to string? //if exists { // typeVal := typeAttr.Value() diff --git a/plan.go b/plan.go index 8458c24..6abc8e3 100644 --- a/plan.go +++ b/plan.go @@ -23,7 +23,7 @@ func planJSONHook(dfs fs.FS, input Input) (func(ctx *tfcontext.Context, blocks t var contents io.Reader = bytes.NewReader(input.PlanJSON) // Also accept `{}` as an empty plan. If this is stored in postgres or another json // type, then `{}` is the "empty" value. - if len(input.PlanJSON) == 0 || bytes.Compare(input.PlanJSON, []byte("{}")) == 0 { + if len(input.PlanJSON) == 0 || bytes.Equal(input.PlanJSON, []byte("{}")) { if input.PlanJSONPath == "" { return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {}, nil } diff --git a/preview.go b/preview.go index 96447ff..b30882f 100644 --- a/preview.go +++ b/preview.go @@ -58,7 +58,7 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn // TODO: Fix logging. There is no way to pass in an instanced logger to // the parser. - //slog.SetLogLoggerLevel(slog.LevelDebug) + // slog.SetLogLoggerLevel(slog.LevelDebug) //slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil))) varFiles, err := tfVarFiles("", dir) @@ -127,7 +127,7 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn }, } } - + outputs := hclext.ExportOutputs(modules) diags := make(hcl.Diagnostics, 0) diff --git a/previewe2e_test.go b/previewe2e_test.go index 4471611..c0c1ad0 100644 --- a/previewe2e_test.go +++ b/previewe2e_test.go @@ -47,7 +47,7 @@ import ( // The goal of the test is to compare `tfstate` with the output of `preview`. // If `preview`'s implementation of terraform is incorrect, the test will fail. // TODO: Adding varied parameter inputs would be a good idea. -// TODO: Add workspace tag comparisions. +// TODO: Add workspace tag comparisons. func Test_VerifyE2E(t *testing.T) { t.Parallel() @@ -135,7 +135,7 @@ func Test_VerifyE2E(t *testing.T) { require.NoError(t, err, "terraform show plan") pd, err := json.Marshal(plan) - require.NoError(t, err, "marshalling plan") + require.NoError(t, err, "marshaling plan") err = os.WriteFile(filepath.Join(wp, "plan.json"), pd, 0644) require.NoError(t, err, "writing plan.json") diff --git a/scripts/rules.go b/scripts/rules.go new file mode 100644 index 0000000..c0793cf --- /dev/null +++ b/scripts/rules.go @@ -0,0 +1,16 @@ +// Package gorules defines custom lint rules for ruleguard. +// +// golangci-lint runs these rules via go-critic, which includes support +// for ruleguard. All Go files in this directory define lint rules +// in the Ruleguard DSL; see: +// +// - https://go-ruleguard.github.io/by-example/ +// - https://pkg.go.dev/github.com/quasilyte/go-ruleguard/dsl +// +// You run one of the following commands to execute your go rules only: +// +// golangci-lint run +// golangci-lint run --disable-all --enable=gocritic +// +// Note: don't forget to run `golangci-lint cache clean`! +package gorules diff --git a/workspacetags.go b/workspacetags.go index 8f7702b..6181c4a 100644 --- a/workspacetags.go +++ b/workspacetags.go @@ -47,7 +47,7 @@ func workspaceTags(modules terraform.Modules, files map[string]*hcl.File) (types continue } - //tagsObj, ok := tagsAttr.HCLAttribute().Expr.(*hclsyntax.ObjectConsExpr) + // tagsObj, ok := tagsAttr.HCLAttribute().Expr.(*hclsyntax.ObjectConsExpr) //if !ok { // diags = diags.Append(&hcl.Diagnostic{ // Severity: hcl.DiagError, @@ -75,7 +75,7 @@ func workspaceTags(modules terraform.Modules, files map[string]*hcl.File) (types return false }) - //for _, item := range tagsObj.Items { + // for _, item := range tagsObj.Items { // tag, tagDiag := newTag(tagsObj, files, item, evCtx) // if tagDiag != nil { // diags = diags.Append(tagDiag) @@ -96,12 +96,12 @@ func workspaceTags(modules terraform.Modules, files map[string]*hcl.File) (types // newTag creates a workspace tag from its hcl expression. func newTag(srcRange *hcl.Range, files map[string]*hcl.File, key, val cty.Value) (types.Tag, *hcl.Diagnostic) { - //key, kdiags := expr.KeyExpr.Value(evCtx) + // key, kdiags := expr.KeyExpr.Value(evCtx) //val, vdiags := expr.ValueExpr.Value(evCtx) // TODO: ??? - //if kdiags.HasErrors() { + // if kdiags.HasErrors() { // key = cty.UnknownVal(cty.String) //} //if vdiags.HasErrors() { @@ -125,7 +125,7 @@ func newTag(srcRange *hcl.Range, files map[string]*hcl.File, key, val cty.Value) if !val.Type().Equals(cty.NilType) { fr = val.Type().FriendlyName() } - //r := expr.ValueExpr.Range() + // r := expr.ValueExpr.Range() return types.Tag{}, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid value type for tag", @@ -150,7 +150,7 @@ func newTag(srcRange *hcl.Range, files map[string]*hcl.File, key, val cty.Value) }, } - //ks, err := source(expr.KeyExpr.Range(), files) + // ks, err := source(expr.KeyExpr.Range(), files) //if err == nil { // src := string(ks) // tag.Key.Source = &src From 45c80025982dfa9775bfeedcbbd136ce58a85f57 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 5 May 2025 16:19:13 -0500 Subject: [PATCH 11/27] chore: golangci-lint linting fixes --- attr.go | 112 ------------------------------------ cli/clidisplay/resources.go | 33 ++++++----- cli/env.go | 6 +- cli/plan.go | 3 +- cli/root.go | 5 +- cli/static/index.html | 55 ------------------ cli/web.go | 17 ++---- cmd/preview/main.go | 1 - hclext/references.go | 7 ++- internal/verify/exec.go | 10 ++-- mark.go | 10 ---- owner.go | 4 +- parameter.go | 2 +- paramhook.go | 8 +-- plan.go | 39 +------------ plan_test.go | 2 + preview.go | 2 +- preview_test.go | 2 + previewe2e_test.go | 3 +- site/genweb/main.go | 1 + source.go | 1 + types/owner.go | 2 +- types/owner_test.go | 14 +++-- types/parameter.go | 2 +- types/primitive.go | 3 +- types/tags.go | 2 +- types/value_test.go | 2 + web/websocket.go | 1 - workspacetags.go | 14 ++--- 29 files changed, 81 insertions(+), 282 deletions(-) delete mode 100644 attr.go delete mode 100644 cli/static/index.html delete mode 100644 mark.go diff --git a/attr.go b/attr.go deleted file mode 100644 index 3d81418..0000000 --- a/attr.go +++ /dev/null @@ -1,112 +0,0 @@ -package preview - -import ( - "fmt" - - "github.com/aquasecurity/trivy/pkg/iac/terraform" - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" -) - -type attributeParser struct { - block *terraform.Block - diags hcl.Diagnostics -} - -func newAttributeParser(block *terraform.Block) *attributeParser { - return &attributeParser{ - block: block, - diags: make(hcl.Diagnostics, 0), - } -} - -func (a *attributeParser) attr(key string) *expectedAttribute { - return &expectedAttribute{ - Key: key, - p: a, - } -} - -type expectedAttribute struct { - Key string - diag hcl.Diagnostics - p *attributeParser -} - -func (a *expectedAttribute) error(diag hcl.Diagnostics) *expectedAttribute { - if a.diag != nil { - return a // already have an error, don't overwrite - } - - a.p.diags = a.p.diags.Extend(diag) - a.diag = diag - return a -} - -func (a *expectedAttribute) required() *expectedAttribute { - attr := a.p.block.GetAttribute(a.Key) - if attr.IsNil() { - r := a.p.block.HCLBlock().Body.MissingItemRange() - a.error(hcl.Diagnostics{ - { - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Missing required attribute %q", a.Key), - // This is the error word for word from 'terraform apply' - Detail: fmt.Sprintf("The argument %q is required, but no definition is found.", a.Key), - Subject: &r, - Extra: nil, - }, - }) - } - - return a -} - -func (a *expectedAttribute) tryString() string { - attr := a.p.block.GetAttribute(a.Key) - if attr.IsNil() { - return "" - } - - if attr.Type() != cty.String { - return "" - } - - return attr.Value().AsString() -} - -func (a *expectedAttribute) string() string { - attr := a.p.block.GetAttribute(a.Key) - if attr.IsNil() { - return "" - } - - if attr.Type() != cty.String { - a.expectedTypeError(attr, "string") - return "" - } - - return attr.Value().AsString() -} - -func (a *expectedAttribute) expectedTypeError(attr *terraform.Attribute, expectedType string) { - var fn string - if attr.IsNil() || attr.Type().Equals(cty.NilType) { - fn = "nil" - } else { - fn = attr.Type().FriendlyName() - } - - a.error(hcl.Diagnostics{ - { - Severity: hcl.DiagError, - Summary: "Invalid attribute type", - Detail: fmt.Sprintf("The attribute %q must be of type %q, found type %q", attr.Name(), expectedType, fn), - Subject: &attr.HCLAttribute().Range, - Context: &a.p.block.HCLBlock().DefRange, - Expression: attr.HCLAttribute().Expr, - - EvalContext: a.p.block.Context().Inner(), - }, - }) -} diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go index ec2ca94..0b4fe49 100644 --- a/cli/clidisplay/resources.go +++ b/cli/clidisplay/resources.go @@ -31,7 +31,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { tableWriter.AppendRow(table.Row{k, v, ""}) continue // diags = diags.Extend(tDiags) - //if !diags.HasErrors() { + // if !diags.HasErrors() { // tableWriter.AppendRow(table.Row{k, v, ""}) // continue //} @@ -42,11 +42,11 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics { tableWriter.AppendRow(table.Row{k, "??", strings.Join(refs, "\n")}) // refs := tb.AllReferences() - //refsStr := make([]string, 0, len(refs)) - //for _, ref := range refs { + // refsStr := make([]string, 0, len(refs)) + // for _, ref := range refs { // refsStr = append(refsStr, ref.String()) //} - //tableWriter.AppendRow(table.Row{unknown, "???", strings.Join(refsStr, "\n")}) + // tableWriter.AppendRow(table.Row{unknown, "???", strings.Join(refsStr, "\n")}) } } _, _ = fmt.Fprintln(writer, tableWriter.Render()) @@ -68,13 +68,13 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc } // value := p.Value.Value // - //if value.IsNull() { + // if value.IsNull() { // strVal = "null" - //} else if !p.Value.Value.IsKnown() { + // } else if !p.Value.Value.IsKnown() { // strVal = "unknown" - //} else if value.Type().Equals(cty.String) { + // } else if value.Type().Equals(cty.String) { // strVal = value.AsString() - //} else { + // } else { // strVal = value.GoString() //} @@ -86,7 +86,6 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc var out bytes.Buffer WriteDiagnostics(&out, files, hcl.Diagnostics(p.Diagnostics)) tableWriter.AppendRow(table.Row{out.String()}) - } tableWriter.AppendSeparator() @@ -100,29 +99,29 @@ func formatOptions(selected []string, options []*types.ParameterOption) string { found := false for _, opt := range options { - str.WriteString(sep) + _, _ = str.WriteString(sep) prefix := "[ ]" if slices.Contains(selected, opt.Value.AsString()) { prefix = "[X]" found = true } - str.WriteString(fmt.Sprintf("%s %s (%s)", prefix, opt.Name, opt.Value.AsString())) + _, _ = str.WriteString(fmt.Sprintf("%s %s (%s)", prefix, opt.Name, opt.Value.AsString())) if opt.Description != "" { - str.WriteString(fmt.Sprintf("\n %s", maxLength(opt.Description, 25))) + _, _ = str.WriteString(fmt.Sprintf("\n %s", maxLength(opt.Description, 25))) } sep = "\n" } if !found { - str.WriteString(sep) - str.WriteString(fmt.Sprintf("= %s", selected)) + _, _ = str.WriteString(sep) + _, _ = str.WriteString(fmt.Sprintf("= %s", selected)) } return str.String() } -func maxLength(s string, max int) string { - if len(s) > max { - return s[:max] + "..." +func maxLength(s string, m int) string { + if len(s) > m { + return s[:m] + "..." } return s } diff --git a/cli/env.go b/cli/env.go index 1cf189f..f86704d 100644 --- a/cli/env.go +++ b/cli/env.go @@ -10,7 +10,7 @@ import ( "github.com/coder/serpent" ) -func (r *RootCmd) SetEnv() *serpent.Command { +func (*RootCmd) SetEnv() *serpent.Command { var ( vars []string groups []string @@ -38,7 +38,7 @@ func (r *RootCmd) SetEnv() *serpent.Command { }, }, Hidden: false, - Handler: func(i *serpent.Invocation) error { + Handler: func(_ *serpent.Invocation) error { for _, val := range vars { parts := strings.Split(val, "=") if len(parts) != 2 { @@ -49,7 +49,7 @@ func (r *RootCmd) SetEnv() *serpent.Command { if err != nil { return err } - fmt.Println("CODER_PARAMETER_" + hex.EncodeToString(sum[:]) + "=" + parts[1]) + _, _ = fmt.Println("CODER_PARAMETER_" + hex.EncodeToString(sum[:]) + "=" + parts[1]) } return nil diff --git a/cli/plan.go b/cli/plan.go index c5e174b..62929c5 100644 --- a/cli/plan.go +++ b/cli/plan.go @@ -10,7 +10,7 @@ import ( "github.com/coder/serpent" ) -func (r *RootCmd) TerraformPlan() *serpent.Command { +func (*RootCmd) TerraformPlan() *serpent.Command { cmd := &serpent.Command{ Use: "plan", Short: "Runs `terraform init -upgrade` and `terraform plan`, saving the output.", @@ -54,6 +54,7 @@ func (r *RootCmd) TerraformPlan() *serpent.Command { var indented bytes.Buffer _ = json.Indent(&indented, buf.Bytes(), "", " ") + //nolint:gosec _ = os.WriteFile("plan.json", indented.Bytes(), 0644) return nil }, diff --git a/cli/root.go b/cli/root.go index 56dfb8d..ab1afc5 100644 --- a/cli/root.go +++ b/cli/root.go @@ -106,11 +106,11 @@ func (r *RootCmd) Root() *serpent.Command { clidisplay.Parameters(os.Stdout, output.Parameters, output.Files) if !output.ModuleOutput.IsNull() && !(output.ModuleOutput.Type().IsObjectType() && output.ModuleOutput.LengthInt() == 0) { - fmt.Println("Module output") + _, _ = fmt.Println("Module output") data, _ := ctyjson.Marshal(output.ModuleOutput, output.ModuleOutput.Type()) var buf bytes.Buffer _ = json.Indent(&buf, data, "", " ") - fmt.Println(buf.String()) + _, _ = fmt.Println(buf.String()) } return nil @@ -122,6 +122,7 @@ func (r *RootCmd) Root() *serpent.Command { return cmd } +//nolint:unused func hclExpr(expr string) hcl.Expression { file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), "test.tf", hcl.InitialPos) if diags.HasErrors() { diff --git a/cli/static/index.html b/cli/static/index.html deleted file mode 100644 index 5fa349f..0000000 --- a/cli/static/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - Codestin Search App - - - - -

WebSocket JSON Preview

-
-
-
-
- -

Output:

-

-
-
diff --git a/cli/web.go b/cli/web.go
index 0b9d9ac..4ffe954 100644
--- a/cli/web.go
+++ b/cli/web.go
@@ -3,7 +3,6 @@ package cli
 import (
 	"bufio"
 	"context"
-	"embed"
 	"encoding/json"
 	"fmt"
 	"io/fs"
@@ -13,6 +12,7 @@ import (
 	"os/exec"
 	"path/filepath"
 	"slices"
+	"time"
 
 	"github.com/go-chi/chi"
 
@@ -24,13 +24,9 @@ import (
 	"github.com/coder/websocket"
 )
 
-//go:embed static/*
-var static embed.FS
-
 type responseRecorder struct {
 	http.ResponseWriter
-	headerWritten bool
-	logger        slog.Logger
+	logger slog.Logger
 }
 
 // Implement Hijacker interface for WebSocket support
@@ -54,7 +50,7 @@ func debugMiddleware(logger slog.Logger) func(http.Handler) http.Handler {
 	}
 }
 
-func (r *RootCmd) WebsocketServer() *serpent.Command {
+func (*RootCmd) WebsocketServer() *serpent.Command {
 	var (
 		address string
 		siteDir string
@@ -132,7 +128,7 @@ func (r *RootCmd) WebsocketServer() *serpent.Command {
 				}
 				_ = json.NewEncoder(rw).Encode(availableUsers)
 			})
-			mux.HandleFunc("/directories", func(rw http.ResponseWriter, r *http.Request) {
+			mux.HandleFunc("/directories", func(rw http.ResponseWriter, _ *http.Request) {
 				entries, err := fs.ReadDir(dataDirFS, ".")
 				if err != nil {
 					http.Error(rw, "Could not read directory", http.StatusInternalServerError)
@@ -163,9 +159,10 @@ func (r *RootCmd) WebsocketServer() *serpent.Command {
 			srv := &http.Server{
 				Addr:    address,
 				Handler: mux,
-				BaseContext: func(listener net.Listener) context.Context {
+				BaseContext: func(_ net.Listener) context.Context {
 					return ctx
 				},
+				ReadHeaderTimeout: time.Second * 30,
 			}
 
 			if siteDir != "" {
@@ -184,7 +181,6 @@ func (r *RootCmd) WebsocketServer() *serpent.Command {
 					// Kill the server if pnpm exits
 					_ = srv.Shutdown(ctx)
 				}()
-
 			}
 
 			logger.Info(ctx, "Starting server", slog.F("address", address))
@@ -197,7 +193,6 @@ func (r *RootCmd) WebsocketServer() *serpent.Command {
 
 func websocketHandler(logger slog.Logger, dirFS fs.FS) func(rw http.ResponseWriter, r *http.Request) {
 	return func(rw http.ResponseWriter, r *http.Request) {
-
 		logger.Debug(r.Context(), "WebSocket connection attempt",
 			slog.F("remote_addr", r.RemoteAddr),
 			slog.F("path", r.URL.Path),
diff --git a/cmd/preview/main.go b/cmd/preview/main.go
index 4adb498..795ee4f 100644
--- a/cmd/preview/main.go
+++ b/cmd/preview/main.go
@@ -30,6 +30,5 @@ func main() {
 			}
 		}
 		log.Fatal(err.Error())
-		os.Exit(1)
 	}
 }
diff --git a/hclext/references.go b/hclext/references.go
index e9f38e9..7da5a0d 100644
--- a/hclext/references.go
+++ b/hclext/references.go
@@ -65,12 +65,13 @@ func CreateDotReferenceFromTraversal(traversals ...hcl.Traversal) string {
 			case hcl.TraverseAttr:
 				refParts = append(refParts, part.Name)
 			case hcl.TraverseIndex:
-				if part.Key.Type().Equals(cty.String) {
+				switch {
+				case part.Key.Type().Equals(cty.String):
 					refParts = append(refParts, fmt.Sprintf("[%s]", part.Key.AsString()))
-				} else if part.Key.Type().Equals(cty.Number) {
+				case part.Key.Type().Equals(cty.Number):
 					idx, _ := part.Key.AsBigFloat().Int64()
 					refParts = append(refParts, fmt.Sprintf("[%d]", idx))
-				} else {
+				default:
 					refParts = append(refParts, fmt.Sprintf("[?? %q]", part.Key.Type().FriendlyName()))
 				}
 			}
diff --git a/internal/verify/exec.go b/internal/verify/exec.go
index 6801e48..1294025 100644
--- a/internal/verify/exec.go
+++ b/internal/verify/exec.go
@@ -129,7 +129,7 @@ func InstallTerraforms(ctx context.Context, t *testing.T, installables ...src.In
 	return execPaths
 }
 
-func LatestTerraformVersion(ctx context.Context) *releases.LatestVersion {
+func LatestTerraformVersion(_ context.Context) *releases.LatestVersion {
 	return &releases.LatestVersion{
 		Product: product.Terraform,
 	}
@@ -158,10 +158,10 @@ func TerraformVersions(ctx context.Context, constraints version.Constraints) ([]
 	}
 
 	include := make([]*releases.ExactVersion, 0)
-	for _, src := range srcs {
-		ev, ok := src.(*releases.ExactVersion)
+	for _, s := range srcs {
+		ev, ok := s.(*releases.ExactVersion)
 		if !ok {
-			return nil, fmt.Errorf("failed to cast src to ExactVersion, type was %T", src)
+			return nil, fmt.Errorf("failed to cast src to ExactVersion, type was %T", s)
 		}
 
 		include = append(include, ev)
@@ -212,7 +212,7 @@ func CopyTFFS(dir string, fsys fs.FS) error {
 		}
 
 		if _, err := io.Copy(w, r); err != nil {
-			w.Close()
+			_ = w.Close()
 			return &os.PathError{Op: "Copy", Path: newPath, Err: err}
 		}
 		return w.Close()
diff --git a/mark.go b/mark.go
deleted file mode 100644
index d97e634..0000000
--- a/mark.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package preview
-
-import (
-	"github.com/hashicorp/hcl/v2"
-	"github.com/zclconf/go-cty/cty"
-)
-
-func markWithDiagnostic(v cty.Value, diag hcl.Diagnostics) cty.Value {
-	return v.Mark(diag)
-}
diff --git a/owner.go b/owner.go
index a300a30..dbd7ad8 100644
--- a/owner.go
+++ b/owner.go
@@ -12,10 +12,10 @@ import (
 func workspaceOwnerHook(_ fs.FS, input Input) (func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value), error) {
 	ownerValue, err := input.Owner.ToCtyValue()
 	if err != nil {
-		return nil, xerrors.Errorf("failed to convert owner value", err)
+		return nil, xerrors.Errorf("failed to convert owner value: %w", err)
 	}
 
-	return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
+	return func(_ *tfcontext.Context, blocks terraform.Blocks, _ map[string]cty.Value) {
 		for _, block := range blocks.OfType("data") {
 			// TODO: Does it have to be me?
 			if block.TypeLabel() == "coder_workspace_owner" && block.NameLabel() == "me" {
diff --git a/parameter.go b/parameter.go
index e56660b..c5d691b 100644
--- a/parameter.go
+++ b/parameter.go
@@ -39,7 +39,7 @@ func parameters(modules terraform.Modules) ([]types.Parameter, hcl.Diagnostics)
 		var detail strings.Builder
 		for _, p := range v {
 			if p.Source != nil {
-				detail.WriteString(fmt.Sprintf("block %q at %s\n",
+				_, _ = detail.WriteString(fmt.Sprintf("block %q at %s\n",
 					p.Source.Type()+"."+strings.Join(p.Source.Labels(), "."),
 					p.Source.HCLBlock().TypeRange))
 			}
diff --git a/paramhook.go b/paramhook.go
index b8c0cd4..7966138 100644
--- a/paramhook.go
+++ b/paramhook.go
@@ -15,7 +15,7 @@ import (
 // is resolvable. The resolvable parameter will be accessible on the next
 // iteration.
 func parameterContextsEvalHook(input Input) func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
-	return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
+	return func(ctx *tfcontext.Context, blocks terraform.Blocks, _ map[string]cty.Value) {
 		data := blocks.OfType("data")
 		for _, block := range data {
 			if block.TypeLabel() != "coder_parameter" {
@@ -157,8 +157,8 @@ func evaluateCoderParameterDefault(b *terraform.Block) (cty.Value, bool) {
 	attributes := b.Attributes()
 
 	// typeAttr, exists := attributes["type"]
-	//valueType := cty.String // TODO: Default to string?
-	//if exists {
+	// valueType := cty.String // TODO: Default to string?
+	// if exists {
 	//	typeVal := typeAttr.Value()
 	//	if !typeVal.Type().Equals(cty.String) || !typeVal.IsWhollyKnown() {
 	//		// TODO: Mark this value somehow
@@ -187,7 +187,7 @@ func evaluateCoderParameterDefault(b *terraform.Block) (cty.Value, bool) {
 	//
 	//// TODO: We should support different tf types, but at present the tf
 	//// schema is static. So only string is allowed
-	//var val cty.Value
+	// var val cty.Value
 
 	def, exists := attributes["default"]
 	if !exists {
diff --git a/plan.go b/plan.go
index 6abc8e3..2714bc8 100644
--- a/plan.go
+++ b/plan.go
@@ -25,7 +25,7 @@ func planJSONHook(dfs fs.FS, input Input) (func(ctx *tfcontext.Context, blocks t
 	// type, then `{}` is the "empty" value.
 	if len(input.PlanJSON) == 0 || bytes.Equal(input.PlanJSON, []byte("{}")) {
 		if input.PlanJSONPath == "" {
-			return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {}, nil
+			return func(_ *tfcontext.Context, _ terraform.Blocks, _ map[string]cty.Value) {}, nil
 		}
 
 		var err error
@@ -40,7 +40,7 @@ func planJSONHook(dfs fs.FS, input Input) (func(ctx *tfcontext.Context, blocks t
 		return nil, fmt.Errorf("unable to parse plan JSON: %w", err)
 	}
 
-	return func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) {
+	return func(_ *tfcontext.Context, blocks terraform.Blocks, _ map[string]cty.Value) {
 		loaded := make(map[*tfjson.StateModule]bool)
 
 		// Do not recurse to child blocks.
@@ -110,22 +110,6 @@ func priorPlanModule(plan *tfjson.Plan, block *terraform.Block) *tfjson.StateMod
 	return current
 }
 
-func matchingBlock(block *terraform.Block, planMod *tfjson.StateModule) *tfjson.StateResource {
-	ref := block.Reference()
-	matchKey := keyMatcher(ref.RawKey())
-
-	for _, resource := range planMod.Resources {
-		if ref.BlockType().ShortName() == string(resource.Mode) &&
-			ref.TypeLabel() == resource.Type &&
-			ref.NameLabel() == resource.Name &&
-			matchKey(resource.Index) {
-
-			return resource
-		}
-	}
-	return nil
-}
-
 func loadResourcesToContext(ctx *tfcontext.Context, resources []*tfjson.StateResource) error {
 	for _, resource := range resources {
 		if resource.Mode != "data" {
@@ -224,24 +208,7 @@ func parsePlanJSON(reader io.Reader) (*tfjson.Plan, error) {
 	return plan, json.NewDecoder(reader).Decode(plan)
 }
 
-func keyMatcher(key cty.Value) func(to any) bool {
-	switch {
-	case key.Type().Equals(cty.Number):
-		idx, _ := key.AsBigFloat().Int64()
-		return func(to any) bool {
-			asInt, ok := toInt(to)
-			return ok && asInt == idx
-		}
-
-	case key.Type().Equals(cty.String):
-		// TODO: handle key strings
-	}
-
-	return func(to any) bool {
-		return true
-	}
-}
-
+//nolint:gosec // Maybe handle overflow at some point
 func toInt(to any) (int64, bool) {
 	switch typed := to.(type) {
 	case uint:
diff --git a/plan_test.go b/plan_test.go
index af268e8..b3d0edd 100644
--- a/plan_test.go
+++ b/plan_test.go
@@ -14,6 +14,8 @@ func TestPlanJSONHook(t *testing.T) {
 	t.Parallel()
 
 	t.Run("Empty plan", func(t *testing.T) {
+		t.Parallel()
+
 		dirFS := os.DirFS("testdata/static")
 		_, diags := preview.Preview(t.Context(), preview.Input{
 			PlanJSONPath:    "",
diff --git a/preview.go b/preview.go
index b30882f..ed51211 100644
--- a/preview.go
+++ b/preview.go
@@ -59,7 +59,7 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
 	// TODO: Fix logging. There is no way to pass in an instanced logger to
 	//   the parser.
 	// slog.SetLogLoggerLevel(slog.LevelDebug)
-	//slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil)))
+	// slog.SetDefault(slog.New(log.NewHandler(os.Stderr, nil)))
 
 	varFiles, err := tfVarFiles("", dir)
 	if err != nil {
diff --git a/preview_test.go b/preview_test.go
index 0c91d36..5bba3a9 100644
--- a/preview_test.go
+++ b/preview_test.go
@@ -504,12 +504,14 @@ func (a assertParam) optVals(opts ...string) assertParam {
 	})
 }
 
+//nolint:unused
 func (a assertParam) opts(opts ...types.ParameterOption) assertParam {
 	return a.extend(func(t *testing.T, parameter types.Parameter) {
 		assert.ElementsMatch(t, opts, parameter.Options, "parameter options equality check")
 	})
 }
 
+//nolint:revive
 func (a assertParam) extend(f assertParam) assertParam {
 	if a == nil {
 		a = func(t *testing.T, parameter types.Parameter) {}
diff --git a/previewe2e_test.go b/previewe2e_test.go
index c0c1ad0..3a9adb2 100644
--- a/previewe2e_test.go
+++ b/previewe2e_test.go
@@ -80,7 +80,7 @@ func Test_VerifyE2E(t *testing.T) {
 			continue
 		}
 
-		entryFiles, err := fs.ReadDir(dirFs, filepath.Join(entry.Name()))
+		entryFiles, err := fs.ReadDir(dirFs, entry.Name())
 		require.NoError(t, err, "reading test data dir")
 		if !slices.ContainsFunc(entryFiles, func(entry fs.DirEntry) bool {
 			return filepath.Ext(entry.Name()) == ".tf"
@@ -137,6 +137,7 @@ func Test_VerifyE2E(t *testing.T) {
 					pd, err := json.Marshal(plan)
 					require.NoError(t, err, "marshaling plan")
 
+					//nolint:gosec // unit test
 					err = os.WriteFile(filepath.Join(wp, "plan.json"), pd, 0644)
 					require.NoError(t, err, "writing plan.json")
 
diff --git a/site/genweb/main.go b/site/genweb/main.go
index 153cb97..fd04236 100644
--- a/site/genweb/main.go
+++ b/site/genweb/main.go
@@ -59,6 +59,7 @@ func main() {
 	}
 
 	if outFile != nil {
+		//nolint:gosec
 		_ = os.WriteFile(*outFile, []byte(output), 0644)
 	} else {
 		_, _ = fmt.Println(output)
diff --git a/source.go b/source.go
index a1c5064..27d22c8 100644
--- a/source.go
+++ b/source.go
@@ -7,6 +7,7 @@ import (
 	"github.com/hashicorp/hcl/v2"
 )
 
+//nolint:unused
 func source(r hcl.Range, files map[string]*hcl.File) ([]byte, error) {
 	file, ok := files[r.Filename]
 	if !ok {
diff --git a/types/owner.go b/types/owner.go
index e714d5f..2962ca7 100644
--- a/types/owner.go
+++ b/types/owner.go
@@ -56,7 +56,7 @@ func (o *WorkspaceOwner) ToCtyValue() (cty.Value, error) {
 		)),
 	}))
 	if err != nil {
-		return cty.Value{}, xerrors.Errorf("failed to convert owner value", err)
+		return cty.Value{}, xerrors.Errorf("failed to convert owner value: %w", err)
 	}
 	return ownerValue, nil
 }
diff --git a/types/owner_test.go b/types/owner_test.go
index d8fb549..3361f3a 100644
--- a/types/owner_test.go
+++ b/types/owner_test.go
@@ -1,13 +1,17 @@
-package types
+package types_test
 
 import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/coder/preview/types"
 )
 
 func TestToCtyValue(t *testing.T) {
-	owner := WorkspaceOwner{
+	t.Parallel()
+
+	owner := types.WorkspaceOwner{
 		ID:           "f6457744-3e16-45b2-b3b0-80c2df491c99",
 		Name:         "Nissa",
 		FullName:     "Nissa, Worldwaker",
@@ -15,7 +19,7 @@ func TestToCtyValue(t *testing.T) {
 		SSHPublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBSHXs/HCgZlpEBOXLvLw4KaOrhy1DM1Vw6M/HPVE/UA\n",
 		Groups:       []string{"Everyone", "Planeswalkers", "Green"},
 		LoginType:    "password",
-		RBACRoles: []WorkspaceOwnerRBACRole{
+		RBACRoles: []types.WorkspaceOwnerRBACRole{
 			{Name: "User Admin"},
 			{Name: "Organization User Admin", OrgID: "5af9253a-ecde-4a71-b8f5-c8d15be9e52b"},
 		},
@@ -38,7 +42,9 @@ func TestToCtyValue(t *testing.T) {
 }
 
 func TestToCtyValueWithNilLists(t *testing.T) {
-	owner := WorkspaceOwner{
+	t.Parallel()
+
+	owner := types.WorkspaceOwner{
 		ID:           "f6457744-3e16-45b2-b3b0-80c2df491c99",
 		Name:         "Nissa",
 		FullName:     "Nissa, Worldwaker",
diff --git a/types/parameter.go b/types/parameter.go
index 6f58ff2..d5fbc67 100644
--- a/types/parameter.go
+++ b/types/parameter.go
@@ -90,7 +90,7 @@ func (v ParameterValidation) Valid(typ string, value string) error {
 		Monotonic:   orZero(v.Monotonic),
 		Regex:       orZero(v.Regex),
 		Error:       v.Error,
-	}).Valid(provider.OptionType(typ), value)
+	}).Valid(typ, value)
 }
 
 type ParameterOption struct {
diff --git a/types/primitive.go b/types/primitive.go
index ae81652..bb8f610 100644
--- a/types/primitive.go
+++ b/types/primitive.go
@@ -42,9 +42,8 @@ func CtyValueString(val cty.Value) (string, error) {
 	case cty.Bool:
 		if val.True() {
 			return "true", nil
-		} else {
-			return "false", nil
 		}
+		return "false", nil
 	case cty.Number:
 		return val.AsBigFloat().String(), nil
 	case cty.String:
diff --git a/types/tags.go b/types/tags.go
index b4cb487..5fadebc 100644
--- a/types/tags.go
+++ b/types/tags.go
@@ -87,7 +87,7 @@ func (t Tag) KeyString() string {
 	return t.Key.AsString()
 }
 
-func (t Tag) AsStrings() (string, string) {
+func (t Tag) AsStrings() (key string, value string) {
 	return t.KeyString(), t.Value.AsString()
 }
 
diff --git a/types/value_test.go b/types/value_test.go
index 099b0bd..fd904d6 100644
--- a/types/value_test.go
+++ b/types/value_test.go
@@ -82,6 +82,8 @@ func TestSafeHCLString(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
 			require.Equal(t, tc.asString, tc.input.AsString())
 			require.Equal(t, tc.known, tc.input.IsKnown(), "known")
 			require.Equal(t, tc.valid, tc.input.Valid(), "valid")
diff --git a/web/websocket.go b/web/websocket.go
index a949199..d0ec8ce 100644
--- a/web/websocket.go
+++ b/web/websocket.go
@@ -25,7 +25,6 @@ func (s *Session) Listen(ctx context.Context, conn *websocket.Conn) {
 	// Always close the connection at the end of the Listen.
 	defer conn.Close(websocket.StatusNormalClosure, "closing connection")
 	<-ctx.Done()
-	return
 }
 
 func (s *Session) readLoop(ctx context.Context, cancel func(), conn *websocket.Conn) {
diff --git a/workspacetags.go b/workspacetags.go
index 6181c4a..d3f6c76 100644
--- a/workspacetags.go
+++ b/workspacetags.go
@@ -48,7 +48,7 @@ func workspaceTags(modules terraform.Modules, files map[string]*hcl.File) (types
 			}
 
 			// tagsObj, ok := tagsAttr.HCLAttribute().Expr.(*hclsyntax.ObjectConsExpr)
-			//if !ok {
+			// if !ok {
 			//	diags = diags.Append(&hcl.Diagnostic{
 			//		Severity: hcl.DiagError,
 			//		Summary:  "Incorrect type for \"tags\" attribute",
@@ -95,16 +95,16 @@ func workspaceTags(modules terraform.Modules, files map[string]*hcl.File) (types
 }
 
 // newTag creates a workspace tag from its hcl expression.
-func newTag(srcRange *hcl.Range, files map[string]*hcl.File, key, val cty.Value) (types.Tag, *hcl.Diagnostic) {
+func newTag(srcRange *hcl.Range, _ map[string]*hcl.File, key, val cty.Value) (types.Tag, *hcl.Diagnostic) {
 	// key, kdiags := expr.KeyExpr.Value(evCtx)
-	//val, vdiags := expr.ValueExpr.Value(evCtx)
+	// val, vdiags := expr.ValueExpr.Value(evCtx)
 
 	// TODO: ???
 
 	// if kdiags.HasErrors() {
 	//	key = cty.UnknownVal(cty.String)
 	//}
-	//if vdiags.HasErrors() {
+	// if vdiags.HasErrors() {
 	//	val = cty.UnknownVal(cty.String)
 	//}
 
@@ -151,13 +151,13 @@ func newTag(srcRange *hcl.Range, files map[string]*hcl.File, key, val cty.Value)
 	}
 
 	// ks, err := source(expr.KeyExpr.Range(), files)
-	//if err == nil {
+	// if err == nil {
 	//	src := string(ks)
 	//	tag.Key.Source = &src
 	//}
 	//
-	//vs, err := source(expr.ValueExpr.Range(), files)
-	//if err == nil {
+	// vs, err := source(expr.ValueExpr.Range(), files)
+	// if err == nil {
 	//	src := string(vs)
 	//	tag.Value.Source = &src
 	//}

From 1be64013cf02deb7fcfd7b805392724362d6af61 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 16:26:23 -0500
Subject: [PATCH 12/27] match coder/coder linter version

---
 .github/workflows/lint.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ade5940..4de789f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,7 +23,7 @@ jobs:
 
       - name: Get golangci-lint cache dir
         run: |
-          linter_ver=1.55.2
+          linter_ver=1.64.8
           go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$linter_ver
           dir=$(golangci-lint cache status | awk '/Dir/ { print $2 }')
           echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV

From abce7d3c5e91a5f06e1b35251103f873aca98010 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 16:45:07 -0500
Subject: [PATCH 13/27] add rules.go to catch a possible panic

---
 extract/parameter.go |  4 ++++
 go.mod               |  1 +
 go.sum               |  2 ++
 hclext/merge.go      |  9 ++++++++-
 hclext/references.go |  1 +
 hclext/vartypes.go   | 43 -------------------------------------------
 paramhook.go         |  1 +
 scripts/rules.go     | 19 +++++++++++++++++++
 types/primitive.go   |  9 +++++++++
 types/value.go       |  1 +
 10 files changed, 46 insertions(+), 44 deletions(-)
 delete mode 100644 hclext/vartypes.go

diff --git a/extract/parameter.go b/extract/parameter.go
index 3851635..6cbd791 100644
--- a/extract/parameter.go
+++ b/extract/parameter.go
@@ -159,6 +159,7 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti
 	if ctyType != cty.NilType && pVal.Value.Type().Equals(cty.String) {
 		// TODO: Wish we could support more types, but only string types are
 		// allowed.
+		//nolint:gocritic // string type asserted
 		valStr := pVal.Value.AsString()
 		// Apply validations to the parameter value
 		for _, v := range p.Validations {
@@ -328,6 +329,7 @@ func requiredString(block *terraform.Block, key string) (string, *hcl.Diagnostic
 		return "", diag
 	}
 
+	// nolint:gocritic // string type asserted
 	return tyVal.AsString(), nil
 }
 
@@ -400,6 +402,7 @@ func nullableString(block *terraform.Block, key string) *string {
 		return nil
 	}
 
+	//nolint:gocritic // string type asserted
 	str := val.AsString()
 	return &str
 }
@@ -414,6 +417,7 @@ func optionalString(block *terraform.Block, key string) string {
 		return ""
 	}
 
+	//nolint:gocritic // string type asserted
 	return val.AsString()
 }
 
diff --git a/go.mod b/go.mod
index 7ea2251..634edb1 100644
--- a/go.mod
+++ b/go.mod
@@ -98,6 +98,7 @@ require (
 	github.com/pion/udp v0.1.4 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/samber/lo v1.49.1 // indirect
diff --git a/go.sum b/go.sum
index c22d751..3cdb559 100644
--- a/go.sum
+++ b/go.sum
@@ -1118,6 +1118,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
diff --git a/hclext/merge.go b/hclext/merge.go
index 8c2919c..caeb762 100644
--- a/hclext/merge.go
+++ b/hclext/merge.go
@@ -1,6 +1,8 @@
 package hclext
 
-import "github.com/zclconf/go-cty/cty"
+import (
+	"github.com/zclconf/go-cty/cty"
+)
 
 func MergeObjects(a, b cty.Value) cty.Value {
 	output := make(map[string]cty.Value)
@@ -9,6 +11,11 @@ func MergeObjects(a, b cty.Value) cty.Value {
 		output[key] = val
 	}
 	b.ForEachElement(func(key, val cty.Value) (stop bool) {
+		// TODO: Should this error be captured?
+		if key.Type() != cty.String {
+			return true
+		}
+		//nolint:gocritic // string type asserted above
 		k := key.AsString()
 		old := output[k]
 		if old.IsKnown() && isNotEmptyObject(old) && isNotEmptyObject(val) {
diff --git a/hclext/references.go b/hclext/references.go
index 7da5a0d..ba23e3f 100644
--- a/hclext/references.go
+++ b/hclext/references.go
@@ -67,6 +67,7 @@ func CreateDotReferenceFromTraversal(traversals ...hcl.Traversal) string {
 			case hcl.TraverseIndex:
 				switch {
 				case part.Key.Type().Equals(cty.String):
+					//nolint:gocritic // string type asserted above
 					refParts = append(refParts, fmt.Sprintf("[%s]", part.Key.AsString()))
 				case part.Key.Type().Equals(cty.Number):
 					idx, _ := part.Key.AsBigFloat().Int64()
diff --git a/hclext/vartypes.go b/hclext/vartypes.go
deleted file mode 100644
index 40c8329..0000000
--- a/hclext/vartypes.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package hclext
-
-import (
-	"github.com/hashicorp/hcl/v2"
-	"github.com/hashicorp/hcl/v2/ext/typeexpr"
-	"github.com/hashicorp/hcl/v2/hclsyntax"
-	"github.com/zclconf/go-cty/cty"
-)
-
-func DecodeVarType(exp hcl.Expression) (cty.Type, *typeexpr.Defaults, error) {
-	// This block converts the string literals "string" -> string
-	// Coder used to allow literal strings, instead of types as keywords. So
-	// we have to handle these cases for backwards compatibility.
-	if tpl, ok := exp.(*hclsyntax.TemplateExpr); ok && len(tpl.Parts) == 1 {
-		if lit, ok := tpl.Parts[0].(*hclsyntax.LiteralValueExpr); ok && lit.Val.Type() == cty.String {
-			keyword := lit.Val.AsString()
-
-			exp = &hclsyntax.ScopeTraversalExpr{
-				Traversal: []hcl.Traverser{
-					hcl.TraverseRoot{
-						Name:     keyword,
-						SrcRange: exp.Range(),
-					},
-				},
-				SrcRange: exp.Range(),
-			}
-		}
-	}
-
-	// Special-case the shortcuts for list(any) and map(any) which aren't hcl.
-	switch hcl.ExprAsKeyword(exp) {
-	case "list":
-		return cty.List(cty.DynamicPseudoType), nil, nil
-	case "map":
-		return cty.Map(cty.DynamicPseudoType), nil, nil
-	}
-
-	t, def, diag := typeexpr.TypeConstraintWithDefaults(exp)
-	if diag.HasErrors() {
-		return cty.NilType, nil, diag
-	}
-	return t, def, nil
-}
diff --git a/paramhook.go b/paramhook.go
index 7966138..03eadc5 100644
--- a/paramhook.go
+++ b/paramhook.go
@@ -32,6 +32,7 @@ func parameterContextsEvalHook(input Input) func(ctx *tfcontext.Context, blocks
 				continue // Ignore the errors at this point
 			}
 
+			//nolint:gocritic // string type asserted
 			name := nameVal.AsString()
 			var value cty.Value
 			pv, ok := input.RichParameterValue(name)
diff --git a/scripts/rules.go b/scripts/rules.go
index c0793cf..8634f12 100644
--- a/scripts/rules.go
+++ b/scripts/rules.go
@@ -14,3 +14,22 @@
 //
 // Note: don't forget to run `golangci-lint cache clean`!
 package gorules
+
+import "github.com/quasilyte/go-ruleguard/dsl"
+
+// asStringsIsDangerous checks for the use of AsString() on cty.Value.
+// This function can panic if not used correctly, so the cty.Type must be known
+// before calling. Ignore this lint if you are confident in your usage.
+func asStringsIsDangerous(m dsl.Matcher) {
+	m.Import("github.com/zclconf/go-cty/cty")
+
+	m.Match(
+		`$v.AsString()`,
+	).
+		Where(
+			m["v"].Type.Is("cty.Value") &&
+				// Ignore unit tests
+				!m.File().Name.Matches(`_test\.go$`),
+		).
+		Report("'AsStrings()' can result in a panic if the type is not known. Ignore this linter with caution")
+}
diff --git a/types/primitive.go b/types/primitive.go
index bb8f610..8f74317 100644
--- a/types/primitive.go
+++ b/types/primitive.go
@@ -7,6 +7,14 @@ import (
 	"github.com/zclconf/go-cty/cty"
 )
 
+func CtyValueStringDefault(def string, val cty.Value) string {
+	str, err := CtyValueString(val)
+	if err != nil {
+		return def
+	}
+	return str
+}
+
 // CtyValueString converts a cty.Value to a string.
 // It supports only primitive types - bool, number, and string.
 // As a special case, it also supports map[string]interface{} with key "value".
@@ -47,6 +55,7 @@ func CtyValueString(val cty.Value) (string, error) {
 	case cty.Number:
 		return val.AsBigFloat().String(), nil
 	case cty.String:
+		//nolint:gocritic // string type asserted above
 		return val.AsString(), nil
 	// We may also have a map[string]interface{} with key "value".
 	case cty.Map(cty.String):
diff --git a/types/value.go b/types/value.go
index 4cc269b..37f862b 100644
--- a/types/value.go
+++ b/types/value.go
@@ -80,6 +80,7 @@ func (s HCLString) AsString() string {
 	if s.Valid() && s.Value.IsKnown() {
 		switch {
 		case s.Value.Type().Equals(cty.String):
+			//nolint:gocritic // string type asserted
 			return s.Value.AsString()
 		case s.Value.Type().Equals(cty.Number):
 			// TODO: Float vs Int?

From 3710fe023aa1bdffe805f1500a8756876269c0de Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 17:00:15 -0500
Subject: [PATCH 14/27] delete more unused code

---
 cli/clidisplay/resources.go | 12 ------------
 cli/plan.go                 |  2 +-
 log.go                      | 15 ---------------
 3 files changed, 1 insertion(+), 28 deletions(-)
 delete mode 100644 log.go

diff --git a/cli/clidisplay/resources.go b/cli/clidisplay/resources.go
index 0b4fe49..e19c3e3 100644
--- a/cli/clidisplay/resources.go
+++ b/cli/clidisplay/resources.go
@@ -30,23 +30,11 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics {
 				k, v := tag.AsStrings()
 				tableWriter.AppendRow(table.Row{k, v, ""})
 				continue
-				// diags = diags.Extend(tDiags)
-				// if !diags.HasErrors() {
-				//	tableWriter.AppendRow(table.Row{k, v, ""})
-				//	continue
-				//}
 			}
 
 			k := tag.KeyString()
 			refs := tag.References()
 			tableWriter.AppendRow(table.Row{k, "??", strings.Join(refs, "\n")})
-
-			// refs := tb.AllReferences()
-			// refsStr := make([]string, 0, len(refs))
-			// for _, ref := range refs {
-			//	refsStr = append(refsStr, ref.String())
-			//}
-			// tableWriter.AppendRow(table.Row{unknown, "???", strings.Join(refsStr, "\n")})
 		}
 	}
 	_, _ = fmt.Fprintln(writer, tableWriter.Render())
diff --git a/cli/plan.go b/cli/plan.go
index 62929c5..c07c498 100644
--- a/cli/plan.go
+++ b/cli/plan.go
@@ -54,7 +54,7 @@ func (*RootCmd) TerraformPlan() *serpent.Command {
 
 			var indented bytes.Buffer
 			_ = json.Indent(&indented, buf.Bytes(), "", "  ")
-			//nolint:gosec
+			//nolint:gosec // these files are not a secret
 			_ = os.WriteFile("plan.json", indented.Bytes(), 0644)
 			return nil
 		},
diff --git a/log.go b/log.go
deleted file mode 100644
index ed1c223..0000000
--- a/log.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package preview
-
-import (
-	"os"
-
-	tlog "github.com/aquasecurity/trivy/pkg/log"
-)
-
-func init() {
-	ll := tlog.New(tlog.NewHandler(os.Stderr, &tlog.Options{
-		Level: tlog.LevelDebug,
-	}))
-	var _ = ll
-	// tlog.SetDefault(ll)
-}

From 0876a7db87f0735012c1122215fe4dace7b3d08b Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 17:11:06 -0500
Subject: [PATCH 15/27] fix AsString to account for other types

---
 types/parameter.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/types/parameter.go b/types/parameter.go
index 6eeee34..e390bfd 100644
--- a/types/parameter.go
+++ b/types/parameter.go
@@ -104,7 +104,7 @@ func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics {
 	var valuePtr *string
 	// TODO: What to do if it is not valid?
 	if value.Valid() {
-		val := value.Value.AsString()
+		val := value.AsString()
 		valuePtr = &val
 	}
 

From b20a5bcc182f543dcf5feb2be1f2140eaf0de18e Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 17:13:52 -0500
Subject: [PATCH 16/27] add skip until provider validation is merged and
 released

---
 testdata/emptydefault/skipe2e | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 testdata/emptydefault/skipe2e

diff --git a/testdata/emptydefault/skipe2e b/testdata/emptydefault/skipe2e
new file mode 100644
index 0000000..63ecd04
--- /dev/null
+++ b/testdata/emptydefault/skipe2e
@@ -0,0 +1 @@
+Skipping until https://github.com/coder/terraform-provider-coder/pull/381 is merged and released
\ No newline at end of file

From 6cf3c102c2ac8f19d5d78eb88166a4c1402acf85 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Mon, 5 May 2025 17:24:53 -0500
Subject: [PATCH 17/27] remove unused code

---
 types/parameter.go | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/types/parameter.go b/types/parameter.go
index e390bfd..91cd8ee 100644
--- a/types/parameter.go
+++ b/types/parameter.go
@@ -76,16 +76,6 @@ type ParameterValidation struct {
 	Monotonic *string `json:"validation_monotonic"`
 }
 
-// Valid takes the type of the value and the value itself and returns an error
-// if the value is invalid.
-func (v *ParameterValidation) Valid(typ string, value string) error {
-	// TODO: Validate typ is the enum?
-	// Use the provider.Validation struct to validate the value to be
-	// consistent with the provider.
-	pv := providerValidation(v)
-	return (&pv).Valid(provider.OptionType(typ), value)
-}
-
 type ParameterOption struct {
 	Name        string    `json:"name"`
 	Description string    `json:"description"`

From ccd1012422bceb7dffe7d978619ce71d0d26ecd3 Mon Sep 17 00:00:00 2001
From: Jaayden Halko 
Date: Tue, 6 May 2025 15:52:50 +0100
Subject: [PATCH 18/27] chore: add type for ParameterStyling

---
 site/src/types/preview.ts | 10 ++++++++--
 types/parameter.go        |  8 +++++++-
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/site/src/types/preview.ts b/site/src/types/preview.ts
index 945c7e1..0d19c4d 100644
--- a/site/src/types/preview.ts
+++ b/site/src/types/preview.ts
@@ -35,8 +35,7 @@ export interface ParameterData {
     readonly type: ParameterType;
     // this is likely an enum in an external package "github.com/coder/terraform-provider-coder/v2/provider.ParameterFormType"
     readonly form_type: string;
-    // empty interface{} type, falling back to unknown
-    readonly styling: unknown;
+    readonly styling: ParameterStyling;
     readonly mutable: boolean;
     readonly default_value: NullHCLString;
     readonly icon: string;
@@ -55,6 +54,13 @@ export interface ParameterOption {
     readonly icon: string;
 }
 
+// From types/parameter.go
+export interface ParameterStyling {
+    readonly placeholder?: string;
+    readonly disabled?: boolean;
+    readonly label?: string;
+}
+
 // From types/enum.go
 export type ParameterType = "bool" | "list(string)" | "number" | "string";
 
diff --git a/types/parameter.go b/types/parameter.go
index d5fbc67..f1f07b0 100644
--- a/types/parameter.go
+++ b/types/parameter.go
@@ -50,7 +50,7 @@ type ParameterData struct {
 	Description  string                     `json:"description"`
 	Type         ParameterType              `json:"type"`
 	FormType     provider.ParameterFormType `json:"form_type"`
-	Styling      any                        `json:"styling"`
+	Styling      ParameterStyling           `json:"styling"`
 	Mutable      bool                       `json:"mutable"`
 	DefaultValue HCLString                  `json:"default_value"`
 	Icon         string                     `json:"icon"`
@@ -76,6 +76,12 @@ type ParameterValidation struct {
 	Invalid   *bool   `json:"validation_invalid"`
 }
 
+type ParameterStyling struct {
+	Placeholder *string `json:"placeholder,omitempty"`
+	Disabled    *bool   `json:"disabled,omitempty"`
+	Label       *string `json:"label,omitempty"`
+}
+
 // Valid takes the type of the value and the value itself and returns an error
 // if the value is invalid.
 func (v ParameterValidation) Valid(typ string, value string) error {

From 87fe1cd89f5b400821d175427ce030210d00bc37 Mon Sep 17 00:00:00 2001
From: Jaayden Halko 
Date: Tue, 6 May 2025 16:14:18 +0100
Subject: [PATCH 19/27] chore: update extract logic for parameter styling

---
 extract/parameter.go | 2 +-
 extract/state.go     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/extract/parameter.go b/extract/parameter.go
index 6cbd791..198173c 100644
--- a/extract/parameter.go
+++ b/extract/parameter.go
@@ -57,7 +57,7 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti
 	}
 
 	ftmeta := optionalString(block, "styling")
-	formTypeMeta := make(map[string]any)
+	var formTypeMeta types.ParameterStyling
 	if ftmeta != "" {
 		_ = json.Unmarshal([]byte(ftmeta), &formTypeMeta)
 	}
diff --git a/extract/state.go b/extract/state.go
index bb1a942..048ae85 100644
--- a/extract/state.go
+++ b/extract/state.go
@@ -54,11 +54,11 @@ func ParameterFromState(block *tfjson.StateResource) (types.Parameter, error) {
 	}
 
 	ftmeta := st.optionalString("styling")
-	var formTypeMeta any
+	var formTypeMeta types.ParameterStyling
 	if ftmeta != "" {
 		_ = json.Unmarshal([]byte(ftmeta), &formTypeMeta)
 	} else {
-		formTypeMeta = map[string]any{}
+		formTypeMeta = types.ParameterStyling{}
 	}
 
 	param := types.Parameter{

From 154d86b5a92a201f86a68de03816c507ef1ab292 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Tue, 6 May 2025 14:53:23 -0500
Subject: [PATCH 20/27] update provider

---
 types/parameter.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/types/parameter.go b/types/parameter.go
index 91cd8ee..5195bcf 100644
--- a/types/parameter.go
+++ b/types/parameter.go
@@ -112,7 +112,7 @@ func (r *ParameterData) Valid(value HCLString) hcl.Diagnostics {
 		Optional:    !r.Required,
 		Order:       int(r.Order),
 		Ephemeral:   r.Ephemeral,
-	}).ValidateInput(valuePtr)
+	}).ValidateInput(valuePtr, nil) // TODO: Pass in previous value
 
 	if diag.HasError() {
 		// TODO: We can take the attr path and decorate the error with

From 753772a63f1520712e5b75ecbf05e777552dd465 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= 
Date: Tue, 6 May 2025 16:37:43 -0600
Subject: [PATCH 21/27] Update testdata/emptydefault/skipe2e

---
 testdata/emptydefault/skipe2e | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/testdata/emptydefault/skipe2e b/testdata/emptydefault/skipe2e
index 63ecd04..bc530d8 100644
--- a/testdata/emptydefault/skipe2e
+++ b/testdata/emptydefault/skipe2e
@@ -1 +1 @@
-Skipping until https://github.com/coder/terraform-provider-coder/pull/381 is merged and released
\ No newline at end of file
+Skipping until https://github.com/coder/terraform-provider-coder/pull/381 is merged and released

From 670b6c8da3e882d984bccbd8bece019dba22703d Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Thu, 8 May 2025 15:18:49 -0500
Subject: [PATCH 22/27] chore: add warnings if modules are missing

Missing or unloaded modules is an incomplete terraform. This warning
is a bit verbose, but signals the functionality will be impacted.
---
 CONTRIBUTING.md                         |  1 +
 preview_test.go                         | 27 +++++++++++++
 testdata/missingmodule/main.tf          | 12 ++++++
 testdata/missingmodule/one/one.tf       |  3 ++
 testdata/missingmodule/one/onea/onea.tf | 14 +++++++
 testdata/missingmodule/skipe2e          |  1 +
 warnings.go                             | 51 +++++++++++++++++++++++++
 7 files changed, 109 insertions(+)
 create mode 100644 testdata/missingmodule/main.tf
 create mode 100644 testdata/missingmodule/one/one.tf
 create mode 100644 testdata/missingmodule/one/onea/onea.tf
 create mode 100644 testdata/missingmodule/skipe2e

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e20c6e5..019e37e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,3 +6,4 @@ To run preview:
 2. `pnpm install`
 3. `cd ..`
 4. `go run ./cmd/preview/main.go web --pnpm=site`
+5. visit http://localhost:5173/?testcontrols=true
diff --git a/preview_test.go b/preview_test.go
index a72dfab..afa1531 100644
--- a/preview_test.go
+++ b/preview_test.go
@@ -5,8 +5,11 @@ import (
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"regexp"
+	"slices"
 	"testing"
 
+	"github.com/hashicorp/hcl/v2"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -38,6 +41,7 @@ func Test_Extract(t *testing.T) {
 		expTags     map[string]string
 		unknownTags []string
 		params      map[string]assertParam
+		warnings    []*regexp.Regexp
 	}{
 		{
 			name:        "bad param values",
@@ -402,6 +406,19 @@ func Test_Extract(t *testing.T) {
 				"beta":  ap().unknown(),
 			},
 		},
+		{
+			name:    "missing_module",
+			dir:     "missingmodule",
+			expTags: map[string]string{},
+			input: preview.Input{
+				ParameterValues: map[string]string{},
+			},
+			unknownTags: []string{},
+			params:      map[string]assertParam{},
+			warnings: []*regexp.Regexp{
+				regexp.MustCompile("Module not loaded"),
+			},
+		},
 		{
 			skip:    "skip until https://github.com/aquasecurity/trivy/pull/8479 is resolved",
 			name:    "submodcount",
@@ -440,6 +457,16 @@ func Test_Extract(t *testing.T) {
 			}
 			require.False(t, diags.HasErrors())
 
+			if len(tc.warnings) > 0 {
+				for _, w := range tc.warnings {
+					idx := slices.IndexFunc(diags, func(diagnostic *hcl.Diagnostic) bool {
+						return w.MatchString(diagnostic.Error())
+
+					})
+					require.Greater(t, idx, -1, "expected warning %q to be present in diags", w.String())
+				}
+			}
+
 			// Assert tags
 			validTags := output.WorkspaceTags.Tags()
 
diff --git a/testdata/missingmodule/main.tf b/testdata/missingmodule/main.tf
new file mode 100644
index 0000000..146e333
--- /dev/null
+++ b/testdata/missingmodule/main.tf
@@ -0,0 +1,12 @@
+module "does-not-exist" {
+  source         = "registry.coder.com/modules/does-not-exist/coder"
+}
+
+module "does-not-exist-2" {
+  count = 0
+  source         = "registry.coder.com/modules/does-not-exist/coder"
+}
+
+module "one" {
+  source = "./one"
+}
\ No newline at end of file
diff --git a/testdata/missingmodule/one/one.tf b/testdata/missingmodule/one/one.tf
new file mode 100644
index 0000000..786d1eb
--- /dev/null
+++ b/testdata/missingmodule/one/one.tf
@@ -0,0 +1,3 @@
+module "onea" {
+  source = "./onea"
+}
\ No newline at end of file
diff --git a/testdata/missingmodule/one/onea/onea.tf b/testdata/missingmodule/one/onea/onea.tf
new file mode 100644
index 0000000..8b0c1f5
--- /dev/null
+++ b/testdata/missingmodule/one/onea/onea.tf
@@ -0,0 +1,14 @@
+terraform {
+  required_providers {
+    coder = {
+      source = "coder/coder"
+      version = "2.4.0-pre0"
+    }
+  }
+}
+
+data "null_data_source" "values" {
+  inputs = {
+    foo = "bar"
+  }
+}
\ No newline at end of file
diff --git a/testdata/missingmodule/skipe2e b/testdata/missingmodule/skipe2e
new file mode 100644
index 0000000..ae4a73a
--- /dev/null
+++ b/testdata/missingmodule/skipe2e
@@ -0,0 +1 @@
+Not a real module
\ No newline at end of file
diff --git a/warnings.go b/warnings.go
index 8a55e3b..7e043b7 100644
--- a/warnings.go
+++ b/warnings.go
@@ -11,6 +11,57 @@ import (
 func warnings(modules terraform.Modules) hcl.Diagnostics {
 	var diags hcl.Diagnostics
 	diags = diags.Extend(unexpandedCountBlocks(modules))
+	diags = diags.Extend(unresolvedModules(modules))
+
+	return diags
+}
+
+// unresolvedModules does a best effort to try and detect if some modules
+// failed to resolve. This is usually because `terraform init` is not run.
+func unresolvedModules(modules terraform.Modules) hcl.Diagnostics {
+	var diags hcl.Diagnostics
+	modulesUsed := make(map[string]bool)
+	modulesByID := make(map[string]*terraform.Block)
+
+	// There is no easy way to know if a `module` failed to resolve. The failure is
+	// only logged in the trivy package. No errors are returned to the caller. So
+	// instead this code will infer a failed resolution by checking if any blocks
+	// exist that reference each `module` block. This will work as long as the module
+	// has some content. If a module is completely empty, then it will be detected as
+	// "not loaded".
+	blocks := modules.GetBlocks()
+	for _, block := range blocks {
+		if block.InModule() && block.ModuleBlock() != nil {
+			modulesUsed[block.ModuleBlock().ID()] = true
+		}
+
+		if block.Type() == "module" {
+			modulesByID[block.ID()] = block
+			_, ok := modulesUsed[block.ID()]
+			if !ok {
+				modulesUsed[block.ID()] = false
+			}
+		}
+	}
+
+	for id, v := range modulesUsed {
+		if !v {
+			block, ok := modulesByID[id]
+			if ok {
+				label := block.Type()
+				for _, l := range block.Labels() {
+					label += " " + fmt.Sprintf("%q", l)
+				}
+
+				diags = diags.Append(&hcl.Diagnostic{
+					Severity: hcl.DiagWarning,
+					Summary:  "Module not loaded, did you run `terraform init`? Or maybe the module is empty.",
+					Detail:   fmt.Sprintf("Module '%s' in file %q cannot be resolved. This module will be ignored.", label, block.HCLBlock().DefRange),
+					Subject:  &(block.HCLBlock().DefRange),
+				})
+			}
+		}
+	}
 
 	return diags
 }

From efcd02c9e82f00116b49af563b4ea88d5ddd6f71 Mon Sep 17 00:00:00 2001
From: Danny Kopping 
Date: Fri, 9 May 2025 13:07:41 +0200
Subject: [PATCH 23/27] chore: update terraform-provider-coder to v2.4.0

Signed-off-by: Danny Kopping 
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 51c118b..b5d930a 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/aquasecurity/trivy v0.58.2
 	github.com/coder/guts v1.0.2-0.20250227211802-139809366a22
 	github.com/coder/serpent v0.10.0
-	github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27
+	github.com/coder/terraform-provider-coder/v2 v2.4.0
 	github.com/coder/websocket v1.8.13
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
diff --git a/go.sum b/go.sum
index 4e42b90..1f8d157 100644
--- a/go.sum
+++ b/go.sum
@@ -718,8 +718,8 @@ github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
 github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
 github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
-github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27 h1:CLJwMqst39+wfFehYQzVOiG5uXUtC5fbAZ3/EpxOWos=
-github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s=
+github.com/coder/terraform-provider-coder/v2 v2.4.0 h1:uuFmF03IyahAZLXEukOdmvV9hGfUMJSESD8+G5wkTcM=
+github.com/coder/terraform-provider-coder/v2 v2.4.0/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s=
 github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
 github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
 github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=

From 9374d44282439c6f6c785c1711d996af841d55b8 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Fri, 9 May 2025 09:22:44 -0500
Subject: [PATCH 24/27] Update warnings.go
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: ケイラ 
---
 warnings.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/warnings.go b/warnings.go
index 7e043b7..781f49a 100644
--- a/warnings.go
+++ b/warnings.go
@@ -55,7 +55,7 @@ func unresolvedModules(modules terraform.Modules) hcl.Diagnostics {
 
 				diags = diags.Append(&hcl.Diagnostic{
 					Severity: hcl.DiagWarning,
-					Summary:  "Module not loaded, did you run `terraform init`? Or maybe the module is empty.",
+					Summary:  "Module not loaded. Did you run `terraform init`?",
 					Detail:   fmt.Sprintf("Module '%s' in file %q cannot be resolved. This module will be ignored.", label, block.HCLBlock().DefRange),
 					Subject:  &(block.HCLBlock().DefRange),
 				})

From 3bfc55455542d35e989a1fda0441e8eb93193390 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Fri, 9 May 2025 09:23:59 -0500
Subject: [PATCH 25/27] newlines to end of file

---
 testdata/missingmodule/main.tf          | 2 +-
 testdata/missingmodule/one/one.tf       | 2 +-
 testdata/missingmodule/one/onea/onea.tf | 2 +-
 testdata/missingmodule/skipe2e          | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/testdata/missingmodule/main.tf b/testdata/missingmodule/main.tf
index 146e333..98839f1 100644
--- a/testdata/missingmodule/main.tf
+++ b/testdata/missingmodule/main.tf
@@ -9,4 +9,4 @@ module "does-not-exist-2" {
 
 module "one" {
   source = "./one"
-}
\ No newline at end of file
+}
diff --git a/testdata/missingmodule/one/one.tf b/testdata/missingmodule/one/one.tf
index 786d1eb..89e1f85 100644
--- a/testdata/missingmodule/one/one.tf
+++ b/testdata/missingmodule/one/one.tf
@@ -1,3 +1,3 @@
 module "onea" {
   source = "./onea"
-}
\ No newline at end of file
+}
diff --git a/testdata/missingmodule/one/onea/onea.tf b/testdata/missingmodule/one/onea/onea.tf
index 8b0c1f5..54718cf 100644
--- a/testdata/missingmodule/one/onea/onea.tf
+++ b/testdata/missingmodule/one/onea/onea.tf
@@ -11,4 +11,4 @@ data "null_data_source" "values" {
   inputs = {
     foo = "bar"
   }
-}
\ No newline at end of file
+}
diff --git a/testdata/missingmodule/skipe2e b/testdata/missingmodule/skipe2e
index ae4a73a..f43c945 100644
--- a/testdata/missingmodule/skipe2e
+++ b/testdata/missingmodule/skipe2e
@@ -1 +1 @@
-Not a real module
\ No newline at end of file
+Not a real module

From 105d830e352d5c062badb81fa193433fccfdd0fe Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Fri, 9 May 2025 09:43:52 -0500
Subject: [PATCH 26/27] chore: update github.com/coder/guts to v1.3.0

---
 go.mod              | 2 +-
 go.sum              | 4 ++--
 site/genweb/main.go | 2 ++
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index b5d930a..34f5855 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.24.2
 require (
 	cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
 	github.com/aquasecurity/trivy v0.58.2
-	github.com/coder/guts v1.0.2-0.20250227211802-139809366a22
+	github.com/coder/guts v1.3.0
 	github.com/coder/serpent v0.10.0
 	github.com/coder/terraform-provider-coder/v2 v2.4.0
 	github.com/coder/websocket v1.8.13
diff --git a/go.sum b/go.sum
index 1f8d157..e3c5bf0 100644
--- a/go.sum
+++ b/go.sum
@@ -712,8 +712,8 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
 github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
 github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
-github.com/coder/guts v1.0.2-0.20250227211802-139809366a22 h1:CACzZc2r5oQj8JKlqiBrMq/MVI7YHqbNndmHJg92B0A=
-github.com/coder/guts v1.0.2-0.20250227211802-139809366a22/go.mod h1:5ducjpIPvmGOcQVigCcXyPFqrH5xeGlZ6c9qPfE1cU4=
+github.com/coder/guts v1.3.0 h1:Kz8LrodQCfz/R06JdCJqdxZDq0BVTTXYYQC/qY3N9fo=
+github.com/coder/guts v1.3.0/go.mod h1:31NO4z6MVTOD4WaCLqE/hUAHGgNok9sRbuMc/LZFopI=
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
 github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
 github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
diff --git a/site/genweb/main.go b/site/genweb/main.go
index fd04236..034afde 100644
--- a/site/genweb/main.go
+++ b/site/genweb/main.go
@@ -68,6 +68,8 @@ func main() {
 
 func TsMutations(ts *guts.Typescript) {
 	ts.ApplyMutations(
+		config.NotNullMaps,
+		config.EnumAsTypes,
 		// Enum list generator
 		config.EnumLists,
 		// Export all top level types

From ff34981a083b754b578cc79d36a8e3c102563c95 Mon Sep 17 00:00:00 2001
From: Steven Masley 
Date: Fri, 9 May 2025 10:38:38 -0500
Subject: [PATCH 27/27] fix: add json unmarshal support for diagnostics

---
 types/diagnostics.go      | 22 ++++++++++++++++++++++
 types/diagnostics_test.go | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 58 insertions(+)
 create mode 100644 types/diagnostics_test.go

diff --git a/types/diagnostics.go b/types/diagnostics.go
index 41d18b7..3ae0955 100644
--- a/types/diagnostics.go
+++ b/types/diagnostics.go
@@ -10,6 +10,28 @@ import (
 // Data is lost when doing a json marshal.
 type Diagnostics hcl.Diagnostics
 
+func (d *Diagnostics) UnmarshalJSON(data []byte) error {
+	cpy := make([]FriendlyDiagnostic, 0)
+	if err := json.Unmarshal(data, &cpy); err != nil {
+		return err
+	}
+
+	*d = make(Diagnostics, 0, len(cpy))
+	for _, diag := range cpy {
+		severity := hcl.DiagError
+		if diag.Severity == DiagnosticSeverityWarning {
+			severity = hcl.DiagWarning
+		}
+
+		*d = append(*d, &hcl.Diagnostic{
+			Severity: severity,
+			Summary:  diag.Summary,
+			Detail:   diag.Detail,
+		})
+	}
+	return nil
+}
+
 func (d Diagnostics) MarshalJSON() ([]byte, error) {
 	cpy := make([]FriendlyDiagnostic, 0, len(d))
 	for _, diag := range d {
diff --git a/types/diagnostics_test.go b/types/diagnostics_test.go
new file mode 100644
index 0000000..82a4845
--- /dev/null
+++ b/types/diagnostics_test.go
@@ -0,0 +1,36 @@
+package types_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/hashicorp/hcl/v2"
+	"github.com/stretchr/testify/require"
+
+	"github.com/coder/preview/types"
+)
+
+func TestDiagnosticsJSON(t *testing.T) {
+
+	diags := types.Diagnostics{
+		{
+			Severity: hcl.DiagWarning,
+			Summary:  "Some summary",
+			Detail:   "Some detail",
+		},
+		{
+			Severity: hcl.DiagError,
+			Summary:  "Some summary",
+			Detail:   "Some detail",
+		},
+	}
+
+	data, err := json.Marshal(diags)
+	require.NoError(t, err, "marshal")
+
+	var newDiags types.Diagnostics
+	err = json.Unmarshal(data, &newDiags)
+	require.NoError(t, err, "unmarshal")
+
+	require.Equal(t, diags, newDiags)
+}