add extra origin information for deprecation diagnostics

This commit is contained in:
Daniel Schmidt 2026-01-12 17:53:13 +01:00
parent 42cee35d38
commit cfe9e3385a
No known key found for this signature in database
GPG key ID: 377C3A4D62FBBBE2
6 changed files with 97 additions and 33 deletions

View file

@ -50,12 +50,16 @@ func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.R
}
for _, depMark := range deprecationMarks {
diags = diags.Append(&hcl.Diagnostic{
diag := &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: depMark.Origin,
}
diag.WrapDiagnosticExtra(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Deprecated value used",
Detail: depMark.Message,
Subject: rng,
})
diags = diags.Append(diag)
}
return notDeprecatedValue, diags
@ -73,13 +77,19 @@ func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tf
for m := range pvm.Marks {
if depMark, ok := m.(marks.DeprecationMark); ok {
diags = diags.Append(
tfdiags.AttributeValue(
tfdiags.Warning,
"Deprecated value used",
depMark.Message,
pvm.Path,
),
)
tfdiags.Override(
tfdiags.AttributeValue(
tfdiags.Warning,
"Deprecated value used",
depMark.Message,
pvm.Path,
),
tfdiags.Warning, // We just want to override the extra info
func() tfdiags.DiagnosticExtraWrapper {
return &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: depMark.Origin,
}
}))
}
}
}

View file

@ -4,6 +4,7 @@
package marks
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
@ -97,6 +98,7 @@ const TypeType = valueMark("TypeType")
// rather than a primitive type so that it can carry a deprecation message.
type DeprecationMark struct {
Message string
Origin *hcl.Range
}
func (d DeprecationMark) GoString() string {
@ -104,10 +106,11 @@ func (d DeprecationMark) GoString() string {
}
// Empty deprecation mark for usage in marks.Has / Contains / etc
var Deprecation = NewDeprecation("")
var Deprecation = NewDeprecation("", nil)
func NewDeprecation(message string) DeprecationMark {
func NewDeprecation(message string, origin *hcl.Range) DeprecationMark {
return DeprecationMark{
Message: message,
Origin: origin,
}
}

View file

@ -10,7 +10,7 @@ import (
)
func TestDeprecationMark(t *testing.T) {
deprecation := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated"))
deprecation := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated", nil))
composite := cty.ObjectVal(map[string]cty.Value{
"foo": deprecation,

View file

@ -32,15 +32,15 @@ func TestPathsWithMark(t *testing.T) {
},
{
Path: cty.GetAttrPath("deprecated"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
},
}
@ -71,15 +71,15 @@ func TestPathsWithMark(t *testing.T) {
},
{
Path: cty.GetAttrPath("deprecated"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
},
}
@ -116,7 +116,7 @@ func TestPathsWithMark(t *testing.T) {
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
},
}
@ -166,15 +166,15 @@ func TestRemoveAll_dataMarks(t *testing.T) {
input := []cty.PathValueMarks{
{
Path: cty.GetAttrPath("deprecated"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
},
}
@ -250,7 +250,7 @@ func TestMarkPaths(t *testing.T) {
cty.GetAttrPath("o").GetAttr("b"),
cty.GetAttrPath("t").IndexInt(0),
}
deprecationMark := NewDeprecation("this is deprecated")
deprecationMark := NewDeprecation("this is deprecated", nil)
got = MarkPaths(value, deprecationMark, deprecatedPaths)
want = cty.ObjectVal(map[string]cty.Value{
"s": cty.StringVal(".s").Mark(deprecationMark),
@ -365,28 +365,28 @@ func TestMarksEqual(t *testing.T) {
},
{
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
},
true,
},
{
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("different"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("different", nil))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("message"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("message", nil))},
},
false,
},
{
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message"))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
},
true,
},

View file

@ -421,7 +421,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
atys[name] = cty.DynamicPseudoType // output values are dynamically-typed
val := cty.UnknownVal(cty.DynamicPseudoType)
if c.DeprecatedSet {
val = val.Mark(marks.NewDeprecation(c.Deprecated))
val = val.Mark(marks.NewDeprecation(c.Deprecated, &c.DeclRange))
}
as[name] = val
}
@ -482,7 +482,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
}
if cfg.DeprecatedSet {
outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated))
outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated, &cfg.DeclRange))
}
attrs[name] = outputVal
}
@ -791,7 +791,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
// states populated for all resources in the configuration.
ret := cty.DynamicVal
if schema.Body.Deprecated {
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type)))
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type), &config.DeclRange))
}
return ret, diags
}
@ -869,7 +869,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
}
if schema.Body.Deprecated {
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type)))
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type), &config.DeclRange))
}
return ret, diags
@ -1152,7 +1152,7 @@ func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.Sour
value = value.Mark(marks.Ephemeral)
}
if config.DeprecatedSet {
value = value.Mark(marks.NewDeprecation(config.Deprecated))
value = value.Mark(marks.NewDeprecation(config.Deprecated, &config.DeclRange))
}
return value, diags

View file

@ -3,6 +3,10 @@
package tfdiags
import (
"github.com/hashicorp/hcl/v2"
)
// This "Extra" idea is something we've inherited from HCL's diagnostic model,
// and so it's primarily to expose that functionality from wrapped HCL
// diagnostics but other diagnostic types could potentially implement this
@ -260,3 +264,50 @@ func DiagnosticCausedByTestFailure(diag Diagnostic) bool {
}
return maybe.DiagnosticCausedByTestFailure()
}
// DiagnosticExtraDeprecationOrigin is an interface implemented by values in
// the Extra field of Diagnostic when the diagnostic is related to a
// deprecation warning. It provides information about the origin of the
// deprecation.
type DiagnosticExtraDeprecationOrigin interface {
DeprecationOrigin() *hcl.Range
}
// DiagnosticDeprecationOrigin returns the origin range of a deprecation
// warning diagnostic, or nil if the diagnostic does not have such information.
func DiagnosticDeprecationOrigin(diag Diagnostic) *hcl.Range {
maybe := ExtraInfo[DiagnosticExtraDeprecationOrigin](diag)
if maybe == nil {
return nil
}
return maybe.DeprecationOrigin()
}
type DeprecationOriginDiagnosticExtra struct {
Origin *hcl.Range
wrapped interface{}
}
var (
_ DiagnosticExtraDeprecationOrigin = (*DeprecationOriginDiagnosticExtra)(nil)
_ DiagnosticExtraWrapper = (*DeprecationOriginDiagnosticExtra)(nil)
_ DiagnosticExtraUnwrapper = (*DeprecationOriginDiagnosticExtra)(nil)
)
func (c *DeprecationOriginDiagnosticExtra) UnwrapDiagnosticExtra() interface{} {
return c.wrapped
}
func (c *DeprecationOriginDiagnosticExtra) WrapDiagnosticExtra(inner interface{}) {
if c.wrapped != nil {
// This is a logical inconsistency, the caller should know whether they
// have already wrapped an extra or not.
panic("Attempted to wrap a diagnostic extra into a DeprecationOriginDiagnosticExtra that is already wrapping a different extra. This is a bug in Terraform, please report it.")
}
c.wrapped = inner
}
func (c *DeprecationOriginDiagnosticExtra) DeprecationOrigin() *hcl.Range {
return c.Origin
}