use a string as deprecation origin to help with indirect references

We want to be able to give better information if e.g. the entire module is stored in a local and the deprecated value is only later used.

Where the diag is emitted we might only see the local and not the true origin of the deprecation

A string identifying the source of the deprecation should help
This commit is contained in:
Daniel Schmidt 2026-01-14 13:30:28 +01:00
parent c9cc64a260
commit 75445e1ef8
11 changed files with 60 additions and 132 deletions

View file

@ -254,14 +254,8 @@ func (f *snippetFormatter) write() {
fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr)
f.writeSnippet(snippet, code)
if diag.DeprecationOriginRange != nil && diag.DeprecationOriginSnippet != nil {
var depContextStr string
if diag.DeprecationOriginSnippet.Context != nil {
depContextStr = fmt.Sprintf(", in %s", *snippet.Context)
}
buf.WriteByte('\n')
fmt.Fprintf(buf, " (origin of deprecation on %s line %d%s):\n", diag.DeprecationOriginRange.Filename, diag.DeprecationOriginRange.Start.Line, depContextStr)
f.writeSnippet(diag.DeprecationOriginSnippet, diag.DeprecationOriginSnippet.Code)
if diag.DeprecationOriginDescription != "" {
fmt.Fprintf(buf, "\n The deprecation originates from %s\n", diag.DeprecationOriginDescription)
}
}

View file

@ -418,11 +418,7 @@ func TestDiagnostic(t *testing.T) {
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Extra: &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: &tfdiags.SourceRange{
Filename: "deprecated.tf",
Start: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
},
OriginDescription: "module.foo.bar",
},
},
`[yellow][reset]
@ -431,8 +427,7 @@ func TestDiagnostic(t *testing.T) {
[yellow][reset] on test.tf line 1:
[yellow][reset] 1: test [underline]source[reset] code
[yellow][reset]
[yellow][reset] (origin of deprecation on deprecated.tf line 1):
[yellow][reset] 1: source of [underline]deprecation[reset]
[yellow][reset] The deprecation originates from module.foo.bar
[yellow][reset]
[yellow][reset] Countermeasures must be taken.
[yellow][reset]
@ -760,11 +755,7 @@ Whatever shall we do?
End: hcl.Pos{Line: 1, Column: 12, Byte: 11},
},
Extra: &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: &tfdiags.SourceRange{
Filename: "deprecated.tf",
Start: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
},
OriginDescription: "module.foo.bar",
},
},
`
@ -773,8 +764,7 @@ Warning: Deprecation detected
on test.tf line 1:
1: test source code
(origin of deprecation on deprecated.tf line 1):
1: source of deprecation
The deprecation originates from module.foo.bar
Countermeasures must be taken.
`,

View file

@ -40,8 +40,7 @@ type Diagnostic struct {
Range *DiagnosticRange `json:"range,omitempty"`
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
DeprecationOriginRange *DiagnosticRange `json:"deprecation_origin_range,omitempty"`
DeprecationOriginSnippet *DiagnosticSnippet `json:"deprecation_origin_snippet,omitempty"`
DeprecationOriginDescription string `json:"deprecation_origin_description,omitempty"`
}
// Pos represents a position in the source code.
@ -403,31 +402,8 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost
}
}
if deprecationOrigin := tfdiags.DiagnosticDeprecationOrigin(diag); deprecationOrigin != nil {
diagnostic.DeprecationOriginRange = &DiagnosticRange{
Filename: deprecationOrigin.Filename,
Start: Pos{
Line: deprecationOrigin.Start.Line,
Column: deprecationOrigin.Start.Column,
Byte: deprecationOrigin.Start.Byte,
},
End: Pos{
Line: deprecationOrigin.End.Line,
Column: deprecationOrigin.End.Column,
Byte: deprecationOrigin.End.Byte,
},
}
var src []byte
if sources != nil {
src = sources[deprecationOrigin.Filename]
}
if src != nil {
highlightRange := deprecationOrigin.ToHCL()
diagnostic.DeprecationOriginSnippet = snippetFromRange(src, highlightRange, highlightRange)
}
if deprecationOrigin := tfdiags.DeprecatedOriginDescription(diag); deprecationOrigin != "" {
diagnostic.DeprecationOriginDescription = deprecationOrigin
}
return diagnostic

View file

@ -880,11 +880,7 @@ func TestNewDiagnostic(t *testing.T) {
End: hcl.Pos{Line: 1, Column: 25, Byte: 24},
},
Extra: &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: &tfdiags.SourceRange{
Filename: "deprecation.tf",
Start: tfdiags.SourcePos{Line: 1, Column: 10, Byte: 9},
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
},
OriginDescription: "aws_s3_bucket.hello.acl",
},
},
&Diagnostic{
@ -912,19 +908,7 @@ func TestNewDiagnostic(t *testing.T) {
HighlightEndOffset: 24,
Values: []DiagnosticExpressionValue{},
},
DeprecationOriginRange: &DiagnosticRange{
Filename: "deprecation.tf",
Start: Pos{Line: 1, Column: 10, Byte: 9},
End: Pos{Line: 1, Column: 25, Byte: 24},
},
DeprecationOriginSnippet: &DiagnosticSnippet{
Context: strPtr(`resource "test_resource" "deprecated"`),
Code: `resource "test_resource" "deprecated" {}`,
StartLine: 1,
HighlightStartOffset: 9,
HighlightEndOffset: 24,
Values: []DiagnosticExpressionValue{},
},
DeprecationOriginDescription: "aws_s3_bucket.hello.acl",
},
},
}

View file

@ -23,25 +23,5 @@
"highlight_end_offset": 24,
"values": []
},
"deprecation_origin_range": {
"filename": "deprecation.tf",
"start": {
"line": 1,
"column": 10,
"byte": 9
},
"end": {
"line": 1,
"column": 25,
"byte": 24
}
},
"deprecation_origin_snippet": {
"context": "resource \"test_resource\" \"deprecated\"",
"code": "resource \"test_resource\" \"deprecated\" {}",
"start_line": 1,
"highlight_start_offset": 9,
"highlight_end_offset": 24,
"values": []
}
"deprecation_origin_description": "aws_s3_bucket.hello.acl"
}

View file

@ -56,11 +56,9 @@ func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.R
Detail: depMark.Message,
Subject: rng,
}
if depMark.Origin != nil {
origin := *depMark.Origin
sourceRange := tfdiags.SourceRangeFromHCL(origin)
if depMark.OriginDescription != "" {
diag.Extra = &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: &sourceRange,
OriginDescription: depMark.OriginDescription,
}
}
diags = diags.Append(diag)
@ -86,15 +84,14 @@ func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tf
depMark.Message,
pvm.Path,
)
origin := depMark.Origin
if origin != nil {
if depMark.OriginDescription != "" {
diag = tfdiags.Override(
diag,
tfdiags.Warning, // We just want to override the extra info
func() tfdiags.DiagnosticExtraWrapper {
sourceRange := tfdiags.SourceRangeFromHCL(*origin)
return &tfdiags.DeprecationOriginDiagnosticExtra{
Origin: &sourceRange,
// TODO: Remove common prefixes from origin descriptions?
OriginDescription: depMark.OriginDescription,
}
})
}

View file

@ -4,7 +4,6 @@
package marks
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
@ -98,7 +97,8 @@ 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
OriginDescription string // a human-readable description of the origin
}
func (d DeprecationMark) GoString() string {
@ -106,11 +106,11 @@ func (d DeprecationMark) GoString() string {
}
// Empty deprecation mark for usage in marks.Has / Contains / etc
var Deprecation = NewDeprecation("", nil)
var Deprecation = NewDeprecation("", "")
func NewDeprecation(message string, origin *hcl.Range) DeprecationMark {
func NewDeprecation(message string, originDescription string) DeprecationMark {
return DeprecationMark{
Message: message,
Origin: origin,
Message: message,
OriginDescription: originDescription,
}
}

View file

@ -10,7 +10,7 @@ import (
)
func TestDeprecationMark(t *testing.T) {
deprecation := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated", nil))
deprecation := cty.StringVal("OldValue").Mark(NewDeprecation("This is outdated", ""))
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", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", ""), "sensitive"),
},
}
@ -71,15 +71,15 @@ func TestPathsWithMark(t *testing.T) {
},
{
Path: cty.GetAttrPath("deprecated"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", ""), "sensitive"),
},
}
@ -116,7 +116,7 @@ func TestPathsWithMark(t *testing.T) {
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", ""), "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", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecations"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil)),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")),
},
{
Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", nil), NewDeprecation("this is also deprecated", nil), "sensitive"),
Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", ""), "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", nil)
deprecationMark := NewDeprecation("this is deprecated", "")
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", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", ""))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", ""))},
},
true,
},
{
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("different", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("different", ""))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("message", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("message", ""))},
},
false,
},
{
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", ""))},
},
[]cty.PathValueMarks{
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", nil))},
{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", ""))},
},
true,
},

View file

@ -421,7 +421,14 @@ 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, &c.DeclRange))
accessor := "."
switch {
case callConfig.Count != nil:
accessor = ".[*]."
case callConfig.ForEach != nil:
accessor = ".[*]."
}
val = val.Mark(marks.NewDeprecation(c.Deprecated, fmt.Sprintf("%s%s%s", addr.String(), accessor, name)))
}
as[name] = val
}
@ -482,7 +489,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
}
if cfg.DeprecatedSet {
outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated, &cfg.DeclRange))
outputVal = outputVal.Mark(marks.NewDeprecation(cfg.Deprecated, fmt.Sprintf("%s.%s", moduleInstAddr.String(), name)))
}
attrs[name] = outputVal
}
@ -791,7 +798,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), &config.DeclRange))
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type), addr.String()))
}
return ret, diags
}
@ -869,7 +876,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), &config.DeclRange))
ret = ret.Mark(marks.NewDeprecation(fmt.Sprintf("Resource %q is deprecated", addr.Type), addr.String()))
}
return ret, diags
@ -1152,7 +1159,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, &config.DeclRange))
value = value.Mark(marks.NewDeprecation(config.Deprecated, addr.Absolute(d.ModulePath).String()))
}
return value, diags

View file

@ -266,21 +266,21 @@ func DiagnosticCausedByTestFailure(diag Diagnostic) bool {
// deprecation warning. It provides information about the origin of the
// deprecation.
type DiagnosticExtraDeprecationOrigin interface {
DeprecationOrigin() *SourceRange
DeprecatedOriginDescription() string
}
// DiagnosticDeprecationOrigin returns the origin range of a deprecation
// warning diagnostic, or nil if the diagnostic does not have such information.
func DiagnosticDeprecationOrigin(diag Diagnostic) *SourceRange {
func DeprecatedOriginDescription(diag Diagnostic) string {
maybe := ExtraInfo[DiagnosticExtraDeprecationOrigin](diag)
if maybe == nil {
return nil
return ""
}
return maybe.DeprecationOrigin()
return maybe.DeprecatedOriginDescription()
}
type DeprecationOriginDiagnosticExtra struct {
Origin *SourceRange
OriginDescription string
wrapped interface{}
}
@ -304,6 +304,6 @@ func (c *DeprecationOriginDiagnosticExtra) WrapDiagnosticExtra(inner interface{}
c.wrapped = inner
}
func (c *DeprecationOriginDiagnosticExtra) DeprecationOrigin() *SourceRange {
return c.Origin
func (c *DeprecationOriginDiagnosticExtra) DeprecatedOriginDescription() string {
return c.OriginDescription
}