diff --git a/internal/command/views/json/diagnostic.go b/internal/command/views/json/diagnostic.go index aeb2bdc6bc..75603f2f99 100644 --- a/internal/command/views/json/diagnostic.go +++ b/internal/command/views/json/diagnostic.go @@ -32,12 +32,16 @@ const ( // information about the source of the diagnostic, this is represented in the // range field. type Diagnostic struct { - Severity string `json:"severity"` - Summary string `json:"summary"` - Detail string `json:"detail"` - Address string `json:"address,omitempty"` - Range *DiagnosticRange `json:"range,omitempty"` - Snippet *DiagnosticSnippet `json:"snippet,omitempty"` + Severity string `json:"severity"` + Summary string `json:"summary"` + Detail string `json:"detail"` + Address string `json:"address,omitempty"` + + Range *DiagnosticRange `json:"range,omitempty"` + Snippet *DiagnosticSnippet `json:"snippet,omitempty"` + + DeprecationOriginRange *DiagnosticRange `json:"deprecation_origin_range,omitempty"` + DeprecationOriginSnippet *DiagnosticSnippet `json:"deprecation_origin_snippet,omitempty"` } // Pos represents a position in the source code. @@ -220,64 +224,7 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost // If we have a source file for the diagnostic, we can emit a code // snippet. if src != nil { - diagnostic.Snippet = &DiagnosticSnippet{ - StartLine: snippetRange.Start.Line, - - // Ensure that the default Values struct is an empty array, as this - // makes consuming the JSON structure easier in most languages. - Values: []DiagnosticExpressionValue{}, - } - - file, offset := parseRange(src, highlightRange) - - // Some diagnostics may have a useful top-level context to add to - // the code snippet output. - contextStr := hcled.ContextString(file, offset-1) - if contextStr != "" { - diagnostic.Snippet.Context = &contextStr - } - - // Build the string of the code snippet, tracking at which byte of - // the file the snippet starts. - var codeStartByte int - sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines) - var code strings.Builder - for sc.Scan() { - lineRange := sc.Range() - if lineRange.Overlaps(snippetRange) { - if codeStartByte == 0 && code.Len() == 0 { - codeStartByte = lineRange.Start.Byte - } - code.Write(lineRange.SliceBytes(src)) - code.WriteRune('\n') - } - } - codeStr := strings.TrimSuffix(code.String(), "\n") - diagnostic.Snippet.Code = codeStr - - // Calculate the start and end byte of the highlight range relative - // to the code snippet string. - start := highlightRange.Start.Byte - codeStartByte - end := start + (highlightRange.End.Byte - highlightRange.Start.Byte) - - // We can end up with some quirky results here in edge cases like - // when a source range starts or ends at a newline character, - // so we'll cap the results at the bounds of the highlight range - // so that consumers of this data don't need to contend with - // out-of-bounds errors themselves. - if start < 0 { - start = 0 - } else if start > len(codeStr) { - start = len(codeStr) - } - if end < 0 { - end = 0 - } else if end > len(codeStr) { - end = len(codeStr) - } - - diagnostic.Snippet.HighlightStartOffset = start - diagnostic.Snippet.HighlightEndOffset = end + diagnostic.Snippet = snippetFromRange(src, highlightRange, snippetRange) if fromExpr := diag.FromExpr(); fromExpr != nil { // We may also be able to generate information about the dynamic @@ -456,9 +403,98 @@ 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) + } + + } + return diagnostic } +func snippetFromRange(src []byte, highlightRange hcl.Range, snippetRange hcl.Range) *DiagnosticSnippet { + snippet := &DiagnosticSnippet{ + StartLine: snippetRange.Start.Line, + + // Ensure that the default Values struct is an empty array, as this + // makes consuming the JSON structure easier in most languages. + Values: []DiagnosticExpressionValue{}, + } + + file, offset := parseRange(src, highlightRange) + + // Some diagnostics may have a useful top-level context to add to + // the code snippet output. + contextStr := hcled.ContextString(file, offset-1) + if contextStr != "" { + snippet.Context = &contextStr + } + + // Build the string of the code snippet, tracking at which byte of + // the file the snippet starts. + var codeStartByte int + sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines) + var code strings.Builder + for sc.Scan() { + lineRange := sc.Range() + if lineRange.Overlaps(snippetRange) { + if codeStartByte == 0 && code.Len() == 0 { + codeStartByte = lineRange.Start.Byte + } + code.Write(lineRange.SliceBytes(src)) + code.WriteRune('\n') + } + } + codeStr := strings.TrimSuffix(code.String(), "\n") + snippet.Code = codeStr + + // Calculate the start and end byte of the highlight range relative + // to the code snippet string. + start := highlightRange.Start.Byte - codeStartByte + end := start + (highlightRange.End.Byte - highlightRange.Start.Byte) + + // We can end up with some quirky results here in edge cases like + // when a source range starts or ends at a newline character, + // so we'll cap the results at the bounds of the highlight range + // so that consumers of this data don't need to contend with + // out-of-bounds errors themselves. + if start < 0 { + start = 0 + } else if start > len(codeStr) { + start = len(codeStr) + } + if end < 0 { + end = 0 + } else if end > len(codeStr) { + end = len(codeStr) + } + + snippet.HighlightStartOffset = start + snippet.HighlightEndOffset = end + return snippet +} + // formatRunBinaryDiag formats the binary expression that caused the failed run diagnostic. // The LHS and RHS values are formatted in a more human-readable way, redacting // sensitive and ephemeral values only for the exact values that hold the mark(s). diff --git a/internal/deprecation/deprecation.go b/internal/deprecation/deprecation.go index ac4e8e65f2..fbac8ca2cc 100644 --- a/internal/deprecation/deprecation.go +++ b/internal/deprecation/deprecation.go @@ -50,12 +50,20 @@ func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.R } for _, depMark := range deprecationMarks { - diags = diags.Append(&hcl.Diagnostic{ + diag := &hcl.Diagnostic{ Severity: hcl.DiagWarning, Summary: "Deprecated value used", Detail: depMark.Message, Subject: rng, - }) + } + if depMark.Origin != nil { + origin := *depMark.Origin + sourceRange := tfdiags.SourceRangeFromHCL(origin) + diag.Extra = &tfdiags.DeprecationOriginDiagnosticExtra{ + Origin: &sourceRange, + } + } + diags = diags.Append(diag) } return notDeprecatedValue, diags @@ -72,14 +80,27 @@ func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tf for _, pvm := range pvms { 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, - ), + diag := tfdiags.AttributeValue( + tfdiags.Warning, + "Deprecated value used", + depMark.Message, + pvm.Path, ) + origin := depMark.Origin + if origin != nil { + 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, + } + }) + } + + diags = diags.Append(diag) + } } } diff --git a/internal/lang/marks/marks.go b/internal/lang/marks/marks.go index 54850ca00d..5b3d3fe0cf 100644 --- a/internal/lang/marks/marks.go +++ b/internal/lang/marks/marks.go @@ -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, } } diff --git a/internal/lang/marks/marks_test.go b/internal/lang/marks/marks_test.go index 8190385d27..8781d1d3ae 100644 --- a/internal/lang/marks/marks_test.go +++ b/internal/lang/marks/marks_test.go @@ -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, diff --git a/internal/lang/marks/paths_test.go b/internal/lang/marks/paths_test.go index 3d21689dbb..558a48110d 100644 --- a/internal/lang/marks/paths_test.go +++ b/internal/lang/marks/paths_test.go @@ -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, }, diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index b22a71c8ad..691a577dc2 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -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 diff --git a/internal/tfdiags/diagnostic_extra.go b/internal/tfdiags/diagnostic_extra.go index 2bf25af830..df06ed6663 100644 --- a/internal/tfdiags/diagnostic_extra.go +++ b/internal/tfdiags/diagnostic_extra.go @@ -260,3 +260,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() *SourceRange +} + +// 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 { + maybe := ExtraInfo[DiagnosticExtraDeprecationOrigin](diag) + if maybe == nil { + return nil + } + return maybe.DeprecationOrigin() +} + +type DeprecationOriginDiagnosticExtra struct { + Origin *SourceRange + + 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() *SourceRange { + return c.Origin +}