diff --git a/internal/command/format/diagnostic.go b/internal/command/format/diagnostic.go index 5915f26bdf7d..65323a1ab806 100644 --- a/internal/command/format/diagnostic.go +++ b/internal/command/format/diagnostic.go @@ -229,7 +229,6 @@ type snippetFormatter struct { func (f *snippetFormatter) write() { diag := f.diag buf := f.buf - color := f.color if diag.Address != "" { fmt.Fprintf(buf, " with %s,\n", diag.Address) } @@ -253,109 +252,120 @@ func (f *snippetFormatter) write() { contextStr = fmt.Sprintf(", in %s", *snippet.Context) } fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr) + f.writeSnippet(snippet, code) - // Split the snippet and render the highlighted section with underlines - start := snippet.HighlightStartOffset - end := snippet.HighlightEndOffset - - // Only buggy diagnostics can have an end range before the start, but - // we need to ensure we don't crash here if that happens. - if end < start { - end = start + 1 - if end > len(code) { - end = len(code) - } + if diag.DeprecationOriginDescription != "" { + fmt.Fprintf(buf, "\n The deprecation originates from %s\n", diag.DeprecationOriginDescription) } + } - // If either start or end is out of range for the code buffer then - // we'll cap them at the bounds just to avoid a panic, although - // this would happen only if there's a bug in the code generating - // the snippet objects. - if start < 0 { - start = 0 - } else if start > len(code) { - start = len(code) - } - if end < 0 { - end = 0 - } else if end > len(code) { + buf.WriteByte('\n') +} + +func (f *snippetFormatter) writeSnippet(snippet *viewsjson.DiagnosticSnippet, code string) { + buf := f.buf + color := f.color + + // Split the snippet and render the highlighted section with underlines + start := snippet.HighlightStartOffset + end := snippet.HighlightEndOffset + + // Only buggy diagnostics can have an end range before the start, but + // we need to ensure we don't crash here if that happens. + if end < start { + end = start + 1 + if end > len(code) { end = len(code) } + } - before, highlight, after := code[0:start], code[start:end], code[end:] - code = fmt.Sprintf(color.Color("%s[underline]%s[reset]%s"), before, highlight, after) - - // Split the snippet into lines and render one at a time - lines := strings.Split(code, "\n") - for i, line := range lines { - fmt.Fprintf( - buf, "%4d: %s\n", - snippet.StartLine+i, - line, - ) - } + // If either start or end is out of range for the code buffer then + // we'll cap them at the bounds just to avoid a panic, although + // this would happen only if there's a bug in the code generating + // the snippet objects. + if start < 0 { + start = 0 + } else if start > len(code) { + start = len(code) + } + if end < 0 { + end = 0 + } else if end > len(code) { + end = len(code) + } + + before, highlight, after := code[0:start], code[start:end], code[end:] + code = fmt.Sprintf(color.Color("%s[underline]%s[reset]%s"), before, highlight, after) + + // Split the snippet into lines and render one at a time + lines := strings.Split(code, "\n") + for i, line := range lines { + fmt.Fprintf( + buf, "%4d: %s\n", + snippet.StartLine+i, + line, + ) + } - if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil { - // The diagnostic may also have information about the dynamic - // values of relevant variables at the point of evaluation. - // This is particularly useful for expressions that get evaluated - // multiple times with different values, such as blocks using - // "count" and "for_each", or within "for" expressions. - values := slices.Clone(snippet.Values) - sort.Slice(values, func(i, j int) bool { - return values[i].Traversal < values[j].Traversal - }) - - fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n")) - if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil { - - fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs) - for i, param := range callInfo.Signature.Params { - if i > 0 { - buf.WriteString(", ") - } - buf.WriteString(param.Name) + if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil { + // The diagnostic may also have information about the dynamic + // values of relevant variables at the point of evaluation. + // This is particularly useful for expressions that get evaluated + // multiple times with different values, such as blocks using + // "count" and "for_each", or within "for" expressions. + values := slices.Clone(snippet.Values) + sort.Slice(values, func(i, j int) bool { + return values[i].Traversal < values[j].Traversal + }) + + fmt.Fprint(buf, color.Color(" [dark_gray]├────────────────[reset]\n")) + if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil { + + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs) + for i, param := range callInfo.Signature.Params { + if i > 0 { + buf.WriteString(", ") } - if param := callInfo.Signature.VariadicParam; param != nil { - if len(callInfo.Signature.Params) > 0 { - buf.WriteString(", ") - } - buf.WriteString(param.Name) - buf.WriteString("...") + buf.WriteString(param.Name) + } + if param := callInfo.Signature.VariadicParam; param != nil { + if len(callInfo.Signature.Params) > 0 { + buf.WriteString(", ") } - buf.WriteString(")\n") + buf.WriteString(param.Name) + buf.WriteString("...") } + buf.WriteString(")\n") + } - // always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output - printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose + // always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output + printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose - // The diagnostic may also have information about failures from test assertions - // in a `terraform test` run. This is useful for understanding the values that - // were being compared when the assertion failed. - // Also, we'll print a JSON diff of the two values to make it easier to see the - // differences. - if snippet.TestAssertionExpr != nil { - f.printTestDiagOutput(snippet.TestAssertionExpr) - } + // The diagnostic may also have information about failures from test assertions + // in a `terraform test` run. This is useful for understanding the values that + // were being compared when the assertion failed. + // Also, we'll print a JSON diff of the two values to make it easier to see the + // differences. + if snippet.TestAssertionExpr != nil { + f.printTestDiagOutput(snippet.TestAssertionExpr) + } - if printValues { - for _, value := range values { - // if the statement is one line, we'll just print it as is - // otherwise, we have to ensure that each line is indented correctly - // and that the first line has the traversal information - valSlice := strings.Split(value.Statement, "\n") - fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), - value.Traversal, valSlice[0]) - - for _, line := range valSlice[1:] { - fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) - } + if printValues { + for _, value := range values { + // if the statement is one line, we'll just print it as is + // otherwise, we have to ensure that each line is indented correctly + // and that the first line has the traversal information + valSlice := strings.Split(value.Statement, "\n") + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), + value.Traversal, valSlice[0]) + + for _, line := range valSlice[1:] { + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) } } } } - buf.WriteByte('\n') } func (f *snippetFormatter) printTestDiagOutput(diag *viewsjson.DiagnosticTestBinaryExpr) { diff --git a/internal/command/format/diagnostic_test.go b/internal/command/format/diagnostic_test.go index 40130e00459b..1c484e53cea2 100644 --- a/internal/command/format/diagnostic_test.go +++ b/internal/command/format/diagnostic_test.go @@ -405,12 +405,39 @@ func TestDiagnostic(t *testing.T) { [red]│[reset] [red]│[reset] Example crash [red]╵[reset] +`, + }, + "warning from deprecation": { + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecation detected", + Detail: "Countermeasures must be taken.", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + Extra: &tfdiags.DeprecationOriginDiagnosticExtra{ + OriginDescription: "module.foo.bar", + }, + }, + `[yellow]╷[reset] +[yellow]│[reset] [bold][yellow]Warning: [reset][bold]Deprecation detected[reset] +[yellow]│[reset] +[yellow]│[reset] on test.tf line 1: +[yellow]│[reset] 1: test [underline]source[reset] code +[yellow]│[reset] +[yellow]│[reset] The deprecation originates from module.foo.bar +[yellow]│[reset] +[yellow]│[reset] Countermeasures must be taken. +[yellow]╵[reset] `, }, } sources := map[string][]byte{ - "test.tf": []byte(`test source code`), + "test.tf": []byte(`test source code`), + "deprecated.tf": []byte(`source of deprecation`), } // This empty Colorize just passes through all of the formatting codes @@ -424,8 +451,9 @@ func TestDiagnostic(t *testing.T) { diag := diags[0] got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40)) want := strings.TrimSpace(test.Want) - if got != want { - t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s\n\n", got, want, diff) } }) } @@ -713,12 +741,39 @@ Error: Bad bad bad 1: test source code Whatever shall we do? +`, + }, + + "warning from deprecation": { + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecation detected", + Detail: "Countermeasures must be taken.", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + Extra: &tfdiags.DeprecationOriginDiagnosticExtra{ + OriginDescription: "module.foo.bar", + }, + }, + ` +Warning: Deprecation detected + + on test.tf line 1: + 1: test source code + + The deprecation originates from module.foo.bar + +Countermeasures must be taken. `, }, } sources := map[string][]byte{ - "test.tf": []byte(`test source code`), + "test.tf": []byte(`test source code`), + "deprecated.tf": []byte(`source of deprecation`), } for name, test := range tests { @@ -728,8 +783,8 @@ Whatever shall we do? diag := diags[0] got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40)) want := strings.TrimSpace(test.Want) - if got != want { - t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n,diff:\n%s\n\n", got, want, diff) } }) } diff --git a/internal/command/views/json/diagnostic.go b/internal/command/views/json/diagnostic.go index aeb2bdc6bc4e..98c6bed99de7 100644 --- a/internal/command/views/json/diagnostic.go +++ b/internal/command/views/json/diagnostic.go @@ -32,12 +32,15 @@ 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"` + + DeprecationOriginDescription string `json:"deprecation_origin_description,omitempty"` } // Pos represents a position in the source code. @@ -220,64 +223,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 +402,75 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost } } + if deprecationOrigin := tfdiags.DeprecatedOriginDescription(diag); deprecationOrigin != "" { + diagnostic.DeprecationOriginDescription = deprecationOrigin + } + 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/command/views/json/diagnostic_test.go b/internal/command/views/json/diagnostic_test.go index f4a32ca89ed7..a2b41521f72c 100644 --- a/internal/command/views/json/diagnostic_test.go +++ b/internal/command/views/json/diagnostic_test.go @@ -48,6 +48,7 @@ func TestNewDiagnostic(t *testing.T) { var.k, ] `), + "deprecation.tf": []byte(`resource "test_resource" "deprecated" {}`), } testCases := map[string]struct { diag interface{} // allow various kinds of diags @@ -867,6 +868,49 @@ func TestNewDiagnostic(t *testing.T) { }, }, }, + + "warning with deprecation origin": { + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value caught in action", + Detail: "Oh no - don't do it!", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + Extra: &tfdiags.DeprecationOriginDiagnosticExtra{ + OriginDescription: "aws_s3_bucket.hello.acl", + }, + }, + &Diagnostic{ + Severity: "warning", + Summary: "Deprecated value caught in action", + Detail: "Oh no - don't do it!", + Range: &DiagnosticRange{ + Filename: "test.tf", + Start: Pos{ + Line: 1, + Column: 10, + Byte: 9, + }, + End: Pos{ + Line: 1, + Column: 25, + Byte: 24, + }, + }, + Snippet: &DiagnosticSnippet{ + Context: strPtr(`resource "test_resource" "test"`), + Code: `resource "test_resource" "test" {`, + StartLine: 1, + HighlightStartOffset: 9, + HighlightEndOffset: 24, + Values: []DiagnosticExpressionValue{}, + }, + DeprecationOriginDescription: "aws_s3_bucket.hello.acl", + }, + }, } for name, tc := range testCases { diff --git a/internal/command/views/json/testdata/diagnostic/warning-with-deprecation-origin.json b/internal/command/views/json/testdata/diagnostic/warning-with-deprecation-origin.json new file mode 100644 index 000000000000..b3a077ab0b78 --- /dev/null +++ b/internal/command/views/json/testdata/diagnostic/warning-with-deprecation-origin.json @@ -0,0 +1,27 @@ +{ + "severity": "warning", + "summary": "Deprecated value caught in action", + "detail": "Oh no - don't do it!", + "range": { + "filename": "test.tf", + "start": { + "line": 1, + "column": 10, + "byte": 9 + }, + "end": { + "line": 1, + "column": 25, + "byte": 24 + } + }, + "snippet": { + "context": "resource \"test_resource\" \"test\"", + "code": "resource \"test_resource\" \"test\" {", + "start_line": 1, + "highlight_start_offset": 9, + "highlight_end_offset": 24, + "values": [] + }, + "deprecation_origin_description": "aws_s3_bucket.hello.acl" +} diff --git a/internal/deprecation/deprecation.go b/internal/deprecation/deprecation.go index ac4e8e65f25c..aa808e06930c 100644 --- a/internal/deprecation/deprecation.go +++ b/internal/deprecation/deprecation.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" @@ -35,33 +36,57 @@ func (d *Deprecations) SuppressModuleCallDeprecation(addr addrs.Module) { d.suppressedModules.Add(addr) } +// Validate checks the given value for deprecation marks and returns diagnostics +// for each deprecation found, unless deprecation warnings are suppressed for the given module. + +// This is only appropriate for non-terminal values (values that can be referenced) and primitive +// values. +// If the value can not be referenced, use ValidateDeep or ValidateAsConfig instead. func (d *Deprecations) Validate(value cty.Value, module addrs.Module, rng *hcl.Range) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics deprecationMarks := marks.GetDeprecationMarks(value) + notDeprecatedValue := marks.RemoveDeprecationMarks(value) + return notDeprecatedValue, d.deprecationMarksToDiagnostics(deprecationMarks, module, rng) +} + +// ValidateDeep does the same as Validate but checks deeply nested deprecation marks as well. +func (d *Deprecations) ValidateDeep(value cty.Value, module addrs.Module, rng *hcl.Range) (cty.Value, tfdiags.Diagnostics) { + deprecationMarks := marks.GetDeprecationMarksDeep(value) + notDeprecatedValue := marks.RemoveDeprecationMarksDeep(value) + return notDeprecatedValue, d.deprecationMarksToDiagnostics(deprecationMarks, module, rng) +} + +func (d *Deprecations) deprecationMarksToDiagnostics(deprecationMarks []marks.DeprecationMark, module addrs.Module, rng *hcl.Range) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics if len(deprecationMarks) == 0 { - return value, diags + return diags } - notDeprecatedValue := marks.RemoveDeprecationMarks(value) - // Check if we need to suppress deprecation warnings for this module call. if d.IsModuleCallDeprecationSuppressed(module) { - return notDeprecatedValue, diags + return diags } 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.OriginDescription != "" { + diag.Extra = &tfdiags.DeprecationOriginDiagnosticExtra{ + OriginDescription: depMark.OriginDescription, + } + } + diags = diags.Append(diag) } - - return notDeprecatedValue, diags + return diags } -func (d *Deprecations) ValidateAsConfig(value cty.Value, module addrs.Module) tfdiags.Diagnostics { +// ValidateAsConfig checks the given value for deprecation marks and returns diagnostics +// for each deprecation found, unless deprecation warnings are suppressed for the given module. +// It checks for deeply nested deprecation marks as well. +func (d *Deprecations) ValidateAsConfig(value cty.Value, schema *configschema.Block, module addrs.Module) tfdiags.Diagnostics { var diags tfdiags.Diagnostics _, pvms := value.UnmarkDeepWithPaths() @@ -72,14 +97,26 @@ 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, ) + if depMark.OriginDescription != "" { + diag = tfdiags.Override( + diag, + tfdiags.Warning, // We just want to override the extra info + func() tfdiags.DiagnosticExtraWrapper { + return &tfdiags.DeprecationOriginDiagnosticExtra{ + // TODO: Remove common prefixes from origin descriptions? + OriginDescription: depMark.OriginDescription, + } + }) + } + + diags = diags.Append(diag) + } } } diff --git a/internal/lang/checks.go b/internal/lang/checks.go index 857784e82700..faa8e622d1de 100644 --- a/internal/lang/checks.go +++ b/internal/lang/checks.go @@ -5,7 +5,6 @@ package lang import ( "fmt" - "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" @@ -22,13 +21,13 @@ import ( // It will either return a non-empty message string or it'll return diagnostics // with either errors or warnings that explain why the given expression isn't // acceptable. -func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAddr *addrs.CheckRule) (string, tfdiags.Diagnostics) { +func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAddr *addrs.CheckRule) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics val, hclDiags := expr.Value(hclCtx) diags = diags.Append(hclDiags) if hclDiags.HasErrors() { - return "", diags + return cty.StringVal(""), diags } val, err := convert.Convert(val, cty.String) @@ -41,10 +40,10 @@ func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAdd Expression: expr, EvalContext: hclCtx, }) - return "", diags + return cty.StringVal(""), diags } if !val.IsKnown() { - return "", diags + return cty.StringVal(""), diags } if val.IsNull() { diags = diags.Append(&hcl.Diagnostic{ @@ -55,10 +54,10 @@ func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAdd Expression: expr, EvalContext: hclCtx, }) - return "", diags + return cty.StringVal(""), diags } - val, valMarks := val.Unmark() + _, valMarks := val.Unmark() if _, sensitive := valMarks[marks.Sensitive]; sensitive { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagWarning, @@ -70,7 +69,7 @@ You can correct this by removing references to sensitive values, or by carefully Expression: expr, EvalContext: hclCtx, }) - return "", diags + return cty.StringVal(""), diags } if _, ephemeral := valMarks[marks.Ephemeral]; ephemeral { @@ -90,22 +89,8 @@ You can correct this by removing references to ephemeral values, or by using the Subject: expr.Range().Ptr(), Extra: extra, }) - return "", diags + return cty.StringVal(""), diags } - if depMarks := marks.FilterDeprecationMarks(valMarks); len(depMarks) > 0 { - for _, depMark := range depMarks { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Deprecated value used", - Detail: depMark.Message, - Subject: expr.Range().Ptr(), - }) - } - } - - // NOTE: We've discarded any other marks the string might have been carrying, - // aside from the sensitive mark. - - return strings.TrimSpace(val.AsString()), diags + return val, diags } diff --git a/internal/lang/marks/marks.go b/internal/lang/marks/marks.go index 54850ca00dad..6421463f6069 100644 --- a/internal/lang/marks/marks.go +++ b/internal/lang/marks/marks.go @@ -64,6 +64,13 @@ func FilterDeprecationMarks(marks cty.ValueMarks) []DeprecationMark { // GetDeprecationMarks returns all deprecation marks present on the given // cty.Value. func GetDeprecationMarks(val cty.Value) []DeprecationMark { + _, marks := val.Unmark() + return FilterDeprecationMarks(marks) +} + +// GetDeprecationMarksDeep returns all deprecation marks present on the given +// cty.Value or any nested values. +func GetDeprecationMarksDeep(val cty.Value) []DeprecationMark { _, marks := val.UnmarkDeep() return FilterDeprecationMarks(marks) } @@ -71,6 +78,20 @@ func GetDeprecationMarks(val cty.Value) []DeprecationMark { // RemoveDeprecationMarks returns a copy of the given cty.Value with all // deprecation marks removed. func RemoveDeprecationMarks(val cty.Value) cty.Value { + newVal, marks := val.Unmark() + + for mark := range marks { + if _, ok := mark.(DeprecationMark); !ok { + newVal = newVal.Mark(mark) + } + } + + return newVal +} + +// RemoveDeprecationMarksDeep returns a copy of the given cty.Value with all +// deprecation marks deeply removed. +func RemoveDeprecationMarksDeep(val cty.Value) cty.Value { newVal, pvms := val.UnmarkDeepWithPaths() otherPvms := RemoveAll(pvms, Deprecation) return newVal.MarkWithPaths(otherPvms) @@ -97,6 +118,8 @@ const TypeType = valueMark("TypeType") // rather than a primitive type so that it can carry a deprecation message. type DeprecationMark struct { Message string + + OriginDescription string // a human-readable description of the origin } func (d DeprecationMark) GoString() string { @@ -104,10 +127,11 @@ func (d DeprecationMark) GoString() string { } // Empty deprecation mark for usage in marks.Has / Contains / etc -var Deprecation = NewDeprecation("") +var Deprecation = NewDeprecation("", "") -func NewDeprecation(message string) DeprecationMark { +func NewDeprecation(message string, originDescription string) DeprecationMark { return DeprecationMark{ - Message: message, + Message: message, + OriginDescription: originDescription, } } diff --git a/internal/lang/marks/marks_test.go b/internal/lang/marks/marks_test.go index 8190385d27c1..fe09a3600d04 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", "")) 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 3d21689dbb1a..a7850ee9c921 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", "")), }, { Path: cty.GetAttrPath("multipleDeprecations"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")), }, { Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "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")), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", "")), }, { Path: cty.GetAttrPath("multipleDeprecations"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")), }, { Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "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"), NewDeprecation("this is also deprecated"), "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")), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", "")), }, { Path: cty.GetAttrPath("multipleDeprecations"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated")), + Marks: cty.NewValueMarks(NewDeprecation("this is deprecated", ""), NewDeprecation("this is also deprecated", "")), }, { Path: cty.GetAttrPath("multipleDeprecationsAndSensitive"), - Marks: cty.NewValueMarks(NewDeprecation("this is deprecated"), NewDeprecation("this is also deprecated"), "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") + 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"))}, + {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"))}, + {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"))}, + {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"))}, + {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"))}, + {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"))}, + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(NewDeprecation("same message", ""))}, }, true, }, diff --git a/internal/moduletest/graph/eval_context.go b/internal/moduletest/graph/eval_context.go index 5e7b92fd2e7b..fdb4a9fb4a96 100644 --- a/internal/moduletest/graph/eval_context.go +++ b/internal/moduletest/graph/eval_context.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "sort" + "strings" "sync" "github.com/hashicorp/hcl/v2" @@ -327,6 +328,9 @@ func (ec *EvalContext) EvaluateRun(run *configs.TestRun, module *configs.Module, errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil) ruleDiags = ruleDiags.Append(moreDiags) + errorMessage, _ = errorMessage.Unmark() + errorMessageStr := strings.TrimSpace(errorMessage.AsString()) + runVal, hclDiags := rule.Condition.Value(hclCtx) ruleDiags = ruleDiags.Append(hclDiags) @@ -390,7 +394,7 @@ func (ec *EvalContext) EvaluateRun(run *configs.TestRun, module *configs.Module, diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Test assertion failed", - Detail: errorMessage, + Detail: errorMessageStr, Subject: rule.Condition.Range().Ptr(), Expression: rule.Condition, EvalContext: hclCtx, diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index e62cecdab696..d7f5eaae5ce2 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -7477,6 +7477,17 @@ output "test_output2" { End: hcl.Pos{Line: 7, Column: 27, Byte: 97}, }, }, + ).Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 7, Column: 10, Byte: 80}, + End: hcl.Pos{Line: 7, Column: 27, Byte: 97}, + }, + }, )) } diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index dc4d0725cd23..e77a1d3621e9 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -4594,3 +4594,73 @@ locals { }, })) } + +func TestContext2Validate_using_module_with_deprecated_output(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "mod/main.tf": ` +output "old" { + deprecated = "Please stop using this" + value = "old" +} + +`, + "main.tf": ` +module "mod" { + source = "./mod" +} + +locals { + m = module.mod # OK - deprecated value is not used +} + +output "test_output" { + value = module.mod.old # WARNING +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + Actions: map[string]*providers.ActionSchema{ + "test_action": { + ConfigSchema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated value used", + Detail: "Please stop using this", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 11, Column: 13, Byte: 143}, + End: hcl.Pos{Line: 11, Column: 27, Byte: 157}, + }, + })) +} diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 1a12f48ca509..3a5810734fa7 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -6,6 +6,7 @@ package terraform import ( "fmt" "log" + "strings" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" @@ -106,7 +107,15 @@ func validateCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalCo errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, &addr) diags = diags.Append(moreDiags) - return errorMessage, hclCtx, diags + _, deprecationDiags := ctx.Deprecations().ValidateDeep(errorMessage, ctx.Path().Module(), rule.ErrorMessage.Range().Ptr()) + diags = diags.Append(deprecationDiags) + + // NOTE: We've discarded any other marks the string might have been carrying, + // aside from the sensitive mark. + errorMessage, _ = errorMessage.Unmark() + errorMessageStr := strings.TrimSpace(errorMessage.AsString()) + + return errorMessageStr, hclCtx, diags } func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) { @@ -161,7 +170,7 @@ func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContex } // We don't care about the returned value here, only the diagnostics - _, deprecationDiags := ctx.Deprecations().Validate(resultVal, addr.ModuleInstance().Module(), rule.Condition.Range().Ptr()) + _, deprecationDiags := ctx.Deprecations().ValidateDeep(resultVal, addr.ModuleInstance().Module(), rule.Condition.Range().Ptr()) diags = diags.Append(deprecationDiags) var err error diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index b22a71c8addb..2c0176dd7249 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -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)) + 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)) + 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))) + 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))) + 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)) + value = value.Mark(marks.NewDeprecation(config.Deprecated, addr.Absolute(d.ModulePath).String())) } return value, diags diff --git a/internal/terraform/node_action_instance.go b/internal/terraform/node_action_instance.go index 4687120d9d29..a2eb8a884ad4 100644 --- a/internal/terraform/node_action_instance.go +++ b/internal/terraform/node_action_instance.go @@ -71,7 +71,7 @@ func (n *NodeActionDeclarationInstance) Execute(ctx EvalContext, _ walkOperation valDiags := validateResourceForbiddenEphemeralValues(ctx, configVal, n.Schema.ConfigSchema) diags = diags.Append(valDiags.InConfigBody(n.Config.Config, n.Addr.String())) - deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()) + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Schema.ConfigSchema, n.ModulePath()) diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String())) if diags.HasErrors() { diff --git a/internal/terraform/node_action_partialexp.go b/internal/terraform/node_action_partialexp.go index 69478d687339..2fdbf1ce2383 100644 --- a/internal/terraform/node_action_partialexp.go +++ b/internal/terraform/node_action_partialexp.go @@ -69,7 +69,7 @@ func (n *NodeActionDeclarationPartialExpanded) Execute(ctx EvalContext, op walkO return diags } - deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.ActionAddr().Module) + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Schema.ConfigSchema, n.ActionAddr().Module) diags = diags.Append(deprecationDiags) if diags.HasErrors() { return diags diff --git a/internal/terraform/node_action_validate.go b/internal/terraform/node_action_validate.go index bbf79f86eeaa..59e85e20d97f 100644 --- a/internal/terraform/node_action_validate.go +++ b/internal/terraform/node_action_validate.go @@ -102,7 +102,7 @@ func (n *NodeValidatableAction) Execute(ctx EvalContext, _ walkOperation) tfdiag } } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.ConfigSchema, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) valDiags = validateResourceForbiddenEphemeralValues(ctx, configVal, schema.ConfigSchema) diags = diags.Append(valDiags.InConfigBody(config, n.Addr.String())) diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index fa68dc2e0d1a..ad08b187d8e3 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -519,7 +519,7 @@ If you do intend to export this data, annotate the output value as sensitive by } if n.Config.DeprecatedSet { - val = marks.RemoveDeprecationMarks(val) + val = marks.RemoveDeprecationMarksDeep(val) if n.Addr.Module.IsRoot() { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -530,7 +530,7 @@ If you do intend to export this data, annotate the output value as sensitive by } } else if n.Config.Expr != nil { var deprecationDiags tfdiags.Diagnostics - val, deprecationDiags = ctx.Deprecations().Validate(val, n.ModulePath(), n.Config.Expr.Range().Ptr()) + val, deprecationDiags = ctx.Deprecations().ValidateDeep(val, n.ModulePath(), n.Config.Expr.Range().Ptr()) diags = diags.Append(deprecationDiags) } diff --git a/internal/terraform/node_provider.go b/internal/terraform/node_provider.go index 0b05761209de..9ed0fe538d45 100644 --- a/internal/terraform/node_provider.go +++ b/internal/terraform/node_provider.go @@ -83,7 +83,7 @@ func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider provi return diags } - deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, n.Addr.Module) + deprecationDiags := ctx.Deprecations().ValidateAsConfig(configVal, configSchema, n.Addr.Module) diags = diags.Append(deprecationDiags.InConfigBody(configBody, n.Addr.String())) if diags.HasErrors() { return diags diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index fcf7650355f7..ac41b3eec745 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -866,7 +866,7 @@ func (n *NodeAbstractResourceInstance) plan( diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(origConfigVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(origConfigVal, schema.Body, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) if diags.HasErrors() { return nil, nil, deferred, keyData, diags } @@ -1773,7 +1773,7 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta.Body, nil, EvalDataForNoInstanceKey) diags = diags.Append(configDiags) diags = diags.Append( - ctx.Deprecations().ValidateAsConfig(metaConfigVal, ctx.Path().Module()).InConfigBody(m.Config, n.Addr.String()), + ctx.Deprecations().ValidateAsConfig(metaConfigVal, providerSchema.ProviderMeta.Body, ctx.Path().Module()).InConfigBody(m.Config, n.Addr.String()), ) metaConfigVal = marks.RemoveDeprecationMarks(metaConfigVal) } @@ -1853,7 +1853,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) diags = diags.Append( - ctx.Deprecations().ValidateAsConfig(configVal, ctx.Path().Module()).InConfigBody(n.Config.Config, n.Addr.String()), + ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, ctx.Path().Module()).InConfigBody(n.Config.Config, n.Addr.String()), ) configVal = marks.RemoveDeprecationMarks(configVal) if diags.HasErrors() { @@ -2194,7 +2194,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned } diags = diags.Append( - ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), ) if diags.HasErrors() { return nil, keyData, diags @@ -2512,7 +2512,7 @@ func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, bo config, _, configDiags := ctx.EvaluateBlock(body, schema, n.ResourceInstanceAddr().Resource, keyData) diags = diags.Append(configDiags) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, n.ModulePath()).InConfigBody(body, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, schema, n.ModulePath()).InConfigBody(body, n.Addr.String())) config = marks.RemoveDeprecationMarks(config) return config, diags @@ -2531,7 +2531,7 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, nil, keyData) config, evalDiags := evalScope.EvalSelfBlock(body, self, schema, keyData) diags = diags.Append(evalDiags) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, n.ModulePath()).InConfigBody(body, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(config, schema, n.ModulePath()).InConfigBody(body, n.Addr.String())) config = marks.RemoveDeprecationMarks(config) return config, diags } diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go index 9823b88243c9..93530ccaf687 100644 --- a/internal/terraform/node_resource_ephemeral.go +++ b/internal/terraform/node_resource_ephemeral.go @@ -76,7 +76,7 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid if diags.HasErrors() { return nil, diags } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, ctx.Path().Module()).InConfigBody(config.Config, inp.addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, ctx.Path().Module()).InConfigBody(config.Config, inp.addr.String())) if diags.HasErrors() { return nil, diags } diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index b7796defbf13..a987bd31073e 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -638,7 +638,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. diags = diags.Append(configDiags) return nil, deferred, diags } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, absAddr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ModulePath()).InConfigBody(n.Config.Config, absAddr.String())) if diags.HasErrors() { return nil, deferred, diags } diff --git a/internal/terraform/node_resource_plan_instance_query.go b/internal/terraform/node_resource_plan_instance_query.go index 3421765adf54..e99879f708bf 100644 --- a/internal/terraform/node_resource_plan_instance_query.go +++ b/internal/terraform/node_resource_plan_instance_query.go @@ -51,7 +51,7 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di return diags } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(blockVal, n.ModulePath()).InConfigBody(config.Config, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(blockVal, schema.FullSchema, n.ModulePath()).InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { return diags } diff --git a/internal/terraform/node_resource_plan_partialexp.go b/internal/terraform/node_resource_plan_partialexp.go index 048d15923a2c..a8c2b34fca87 100644 --- a/internal/terraform/node_resource_plan_partialexp.go +++ b/internal/terraform/node_resource_plan_partialexp.go @@ -200,7 +200,7 @@ func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalCo return &change, diags } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) if diags.HasErrors() { return &change, diags } @@ -359,7 +359,7 @@ func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalConte return &change, diags } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ResourceAddr().Module).InConfigBody(n.config.Config, n.addr.String())) if diags.HasErrors() { return &change, diags } diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index 8028dd1e5c28..c918cb32f491 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -144,7 +144,7 @@ func (n *NodeValidatableResource) evaluateBlock(ctx EvalContext, body hcl.Body, keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil) val, hclBody, diags := ctx.EvaluateBlock(body, schema, selfAddr, keyData) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(val, n.Addr.Module).InConfigBody(body, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(val, schema, n.Addr.Module).InConfigBody(body, n.Addr.String())) return marks.RemoveDeprecationMarks(val), hclBody, diags } @@ -360,7 +360,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String())) if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range n.Config.Managed.IgnoreChanges { @@ -440,7 +440,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag diags = diags.Append( validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), ) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath())) + diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ModulePath())) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() @@ -469,7 +469,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } diags = diags.Append( - ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), + ctx.Deprecations().ValidateAsConfig(configVal, schema.Body, n.ModulePath()).InConfigBody(n.Config.Config, n.Addr.String()), ) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() @@ -479,7 +479,6 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag } resp := provider.ValidateEphemeralResourceConfig(req) - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(configVal, n.ModulePath())) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) case addrs.ListResourceMode: schema := providerSchema.SchemaForListResourceType(n.Config.Type) @@ -493,34 +492,40 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - blockVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.FullSchema, nil, keyData) - diags = diags.Append(valDiags) - if valDiags.HasErrors() { - return diags - } - diags = diags.Append(ctx.Deprecations().ValidateAsConfig(blockVal, n.ModulePath())) + var blockVal, limit, includeResource cty.Value + var includeDiags tfdiags.Diagnostics - limit, _, limitDiags := newLimitEvaluator(true).EvaluateExpr(ctx, n.Config.List.Limit) - diags = diags.Append(limitDiags) - if limitDiags.HasErrors() { - return diags + if n.Config.Config != nil { + var valDiags tfdiags.Diagnostics + blockVal, _, valDiags = ctx.EvaluateBlock(n.Config.Config, schema.FullSchema, nil, keyData) + diags = diags.Append(valDiags) + if valDiags.HasErrors() { + return diags + } + deprecationDiags := ctx.Deprecations().ValidateAsConfig(blockVal, schema.FullSchema, n.ModulePath()) + diags = diags.Append(deprecationDiags.InConfigBody(n.Config.Config, n.Addr.String())) } + if n.Config.List.Limit != nil { - var limitDeprecationDiags tfdiags.Diagnostics - limit, limitDeprecationDiags = ctx.Deprecations().Validate(limit, n.ModulePath(), n.Config.List.Limit.Range().Ptr()) - diags = diags.Append(limitDeprecationDiags) + var limitDiags tfdiags.Diagnostics + limit, _, limitDiags = newLimitEvaluator(true).EvaluateExpr(ctx, n.Config.List.Limit) + diags = diags.Append(limitDiags) + if limitDiags.HasErrors() { + return diags + } + _, deprecationDiags := ctx.Deprecations().Validate(limit, n.ModulePath(), n.Config.List.Limit.Range().Ptr()) + diags = diags.Append(deprecationDiags) limit = marks.RemoveDeprecationMarks(limit) } - includeResource, _, includeDiags := newIncludeRscEvaluator(true).EvaluateExpr(ctx, n.Config.List.IncludeResource) - diags = diags.Append(includeDiags) - if includeDiags.HasErrors() { - return diags - } if n.Config.List.IncludeResource != nil { - var includeDeprecationDiags tfdiags.Diagnostics - includeResource, includeDeprecationDiags = ctx.Deprecations().Validate(includeResource, n.ModulePath(), n.Config.List.IncludeResource.Range().Ptr()) - diags = diags.Append(includeDeprecationDiags) + includeResource, _, includeDiags = newIncludeRscEvaluator(true).EvaluateExpr(ctx, n.Config.List.IncludeResource) + diags = diags.Append(includeDiags) + if includeDiags.HasErrors() { + return diags + } + _, deprecationDiags := ctx.Deprecations().Validate(includeResource, n.ModulePath(), n.Config.List.IncludeResource.Range().Ptr()) + diags = diags.Append(deprecationDiags) includeResource = marks.RemoveDeprecationMarks(includeResource) } diff --git a/internal/tfdiags/contextual.go b/internal/tfdiags/contextual.go index 3eedf82bfefe..0b6f27b69ff7 100644 --- a/internal/tfdiags/contextual.go +++ b/internal/tfdiags/contextual.go @@ -42,9 +42,19 @@ func (diags Diagnostics) InConfigBody(body hcl.Body, addr string) Diagnostics { ret := make(Diagnostics, len(diags)) for i, srcDiag := range diags { - if cd, isCD := srcDiag.(contextualFromConfigBody); isCD { - ret[i] = cd.ElaborateFromConfigBody(body, addr) - } else { + switch diag := srcDiag.(type) { + case contextualFromConfigBody: + ret[i] = diag.ElaborateFromConfigBody(body, addr) + case overriddenDiagnostic: + if cd, isCD := diag.original.(contextualFromConfigBody); isCD { + newOriginal := cd.ElaborateFromConfigBody(body, addr) + ret[i] = &overriddenDiagnostic{ + original: newOriginal, + severity: diag.severity, + extra: diag.extra, + } + } + default: ret[i] = srcDiag } } @@ -146,7 +156,6 @@ func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string // presence of errors where performance isn't a concern. traverse := d.attrPath[:] - final := d.attrPath[len(d.attrPath)-1] // Index should never be the first step // as indexing of top blocks (such as resources & data sources) @@ -157,54 +166,19 @@ func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string return &ret } - // Process index separately - idxStep, hasIdx := final.(cty.IndexStep) - if hasIdx { - final = d.attrPath[len(d.attrPath)-2] - traverse = d.attrPath[:len(d.attrPath)-1] - } - // If we have more than one step after removing index // then we'll first try to traverse to a child body // corresponding to the requested path. + remaining := traverse if len(traverse) > 1 { - body = traversePathSteps(traverse, body) + body, remaining = getDeepestBodyFromPath(body, traverse) } // Default is to indicate a missing item in the deepest body we reached // while traversing. - subject := SourceRangeFromHCL(body.MissingItemRange()) + subject := SourceRangeFromHCL(rangeOfDeepestAttributeValueFromPath(body, remaining)) ret.subject = &subject - // Once we get here, "final" should be a GetAttr step that maps to an - // attribute in our current body. - finalStep, isAttr := final.(cty.GetAttrStep) - if !isAttr { - return &ret - } - - content, _, contentDiags := body.PartialContent(&hcl.BodySchema{ - Attributes: []hcl.AttributeSchema{ - { - Name: finalStep.Name, - Required: true, - }, - }, - }) - if contentDiags.HasErrors() { - return &ret - } - - if attr, ok := content.Attributes[finalStep.Name]; ok { - hclRange := attr.Expr.Range() - if hasIdx { - // Try to be more precise by finding index range - hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr) - } - subject = SourceRangeFromHCL(hclRange) - ret.subject = &subject - } - return &ret } @@ -233,13 +207,15 @@ func (d *attributeDiagnostic) Equals(otherDiag ComparableDiagnostic) bool { return sourceRangeEquals(d.subject, od.subject) } -func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { +func getDeepestBodyFromPath(body hcl.Body, traverse []cty.PathStep) (hcl.Body, []cty.PathStep) { + lastProcessedIndex := -1 + +LOOP: for i := 0; i < len(traverse); i++ { step := traverse[i] switch tStep := step.(type) { case cty.GetAttrStep: - var next cty.PathStep if i < (len(traverse) - 1) { next = traverse[i+1] @@ -271,7 +247,7 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { }, }) if contentDiags.HasErrors() { - return body + break LOOP } filtered := make([]*hcl.Block, 0, len(content.Blocks)) for _, block := range content.Blocks { @@ -281,22 +257,24 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { } if len(filtered) == 0 { // Step doesn't refer to a block - continue + break LOOP } switch indexType { case cty.NilType: // no index at all if len(filtered) != 1 { - return body + break LOOP } body = filtered[0].Body + lastProcessedIndex = i case cty.Number: var idx int err := gocty.FromCtyValue(indexVal, &idx) if err != nil || idx >= len(filtered) { - return body + break LOOP } body = filtered[idx].Body + lastProcessedIndex = i case cty.String: key := indexVal.AsString() var block *hcl.Block @@ -309,56 +287,109 @@ func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { if block == nil { // No block with this key, so we'll just indicate a // missing item in the containing block. - return body + break LOOP } body = block.Body + lastProcessedIndex = i default: // Should never happen, because only string and numeric indices // are supported by cty collections. - return body + break LOOP } default: // For any other kind of step, we'll just return our current body // as the subject and accept that this is a little inaccurate. - return body + break LOOP } } - return body + return body, traverse[lastProcessedIndex+1:] } -func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range { - switch idxStep.Key.Type() { - case cty.Number: - var idx int - err := gocty.FromCtyValue(idxStep.Key, &idx) - items, diags := hcl.ExprList(attr.Expr) - if diags.HasErrors() { - return attr.Expr.Range() +func rangeOfDeepestAttributeValueFromPath(body hcl.Body, traverse cty.Path) hcl.Range { + if len(traverse) == 0 { + return body.MissingItemRange() + } + // First we need to use the first traverse item to get the final attribute + // expression. + current, rest := traverse[0], traverse[1:] + + currentGetAttr, ok := current.(cty.GetAttrStep) + if !ok { + // If the remaining basis is not an attribute access something went wrong. + // We can't do anything better than returning the bodies missing item range. + return body.MissingItemRange() + } + + content, _, contentDiags := body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: currentGetAttr.Name, + Required: true, + }, + }, + }) + if contentDiags.HasErrors() { + return body.MissingItemRange() + } + attr, ok := content.Attributes[currentGetAttr.Name] + if !ok { + // We could not find the attribute, this should have emitted a diag above, but just in case + return body.MissingItemRange() + } + + // Now we need to loop through the rest of the path and progressively introspect + // the HCL expression. + currentExpr := attr.Expr + +STEP_ITERATION: + for _, step := range rest { + // We treat cty.IndexStep[type=String] and cty.GetAttrStep the same, so we just + // need to deal with list indexes first + if idxStep, ok := step.(cty.IndexStep); ok && idxStep.Key.Type() == cty.Number { + var idx int + err := gocty.FromCtyValue(idxStep.Key, &idx) + items, diags := hcl.ExprList(currentExpr) + if diags.HasErrors() { + return currentExpr.Range() + } + if err != nil || idx >= len(items) { + return attr.NameRange + } + currentExpr = items[idx] + continue STEP_ITERATION } - if err != nil || idx >= len(items) { - return attr.NameRange + + var stepKey string + switch s := step.(type) { + case cty.GetAttrStep: + stepKey = s.Name + case cty.IndexStep: + stepKey = s.Key.AsString() + default: // should not happen + return currentExpr.Range() } - return items[idx].Range() - case cty.String: - pairs, diags := hcl.ExprMap(attr.Expr) + + pairs, diags := hcl.ExprMap(currentExpr) if diags.HasErrors() { - return attr.Expr.Range() + return currentExpr.Range() } - stepKey := idxStep.Key.AsString() + for _, kvPair := range pairs { key, diags := kvPair.Key.Value(nil) if diags.HasErrors() { - return attr.Expr.Range() + return currentExpr.Range() } if key.AsString() == stepKey { - startRng := kvPair.Value.StartRange() - return startRng + currentExpr = kvPair.Value + continue STEP_ITERATION } } + // If we could not find the item return early return attr.NameRange } - return attr.Expr.Range() + + return currentExpr.Range() } func (d *attributeDiagnostic) Source() Source { diff --git a/internal/tfdiags/contextual_test.go b/internal/tfdiags/contextual_test.go index 7395e731eeea..82313c27aaf6 100644 --- a/internal/tfdiags/contextual_test.go +++ b/internal/tfdiags/contextual_test.go @@ -38,6 +38,7 @@ parent { nested_map = { first_key = "first_value" second_key = "2nd value" + third_key = [{ num = 1}, {num = 2, value = { another_level = ["no", "yes"]}}] } } tuple_of_one = ["one"] @@ -323,8 +324,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 22, Column: 19, Byte: 266}, - End: SourcePos{Line: 22, Column: 30, Byte: 277}, + Start: SourcePos{Line: 22, Column: 18, Byte: 265}, + End: SourcePos{Line: 22, Column: 31, Byte: 278}, }, }, { @@ -340,8 +341,28 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 23, Column: 19, Byte: 297}, - End: SourcePos{Line: 23, Column: 28, Byte: 306}, + Start: SourcePos{Line: 23, Column: 18, Byte: 296}, + End: SourcePos{Line: 23, Column: 29, Byte: 307}, + }, + }, + { + AttributeValue( + Error, + "parent.nested_map.third_key[1].value[1]", + "detail", + cty.Path{ + cty.GetAttrStep{Name: "parent"}, + cty.GetAttrStep{Name: "nested_map"}, + cty.IndexStep{Key: cty.StringVal("third_key")}, + cty.IndexStep{Key: cty.NumberIntVal(1)}, + cty.GetAttrStep{Name: "value"}, + cty.IndexStep{Key: cty.NumberIntVal(1)}, + }, + ), + &SourceRange{ + Filename: "test.tf", + Start: SourcePos{Line: 24, Column: 48, Byte: 355}, + End: SourcePos{Line: 24, Column: 80, Byte: 387}, }, }, { @@ -375,8 +396,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 26, Column: 17, Byte: 330}, - End: SourcePos{Line: 26, Column: 22, Byte: 335}, + Start: SourcePos{Line: 27, Column: 17, Byte: 412}, + End: SourcePos{Line: 27, Column: 22, Byte: 417}, }, }, { @@ -391,8 +412,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 27, Column: 17, Byte: 353}, - End: SourcePos{Line: 27, Column: 24, Byte: 360}, + Start: SourcePos{Line: 28, Column: 17, Byte: 435}, + End: SourcePos{Line: 28, Column: 24, Byte: 442}, }, }, { @@ -407,8 +428,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 27, Column: 26, Byte: 362}, - End: SourcePos{Line: 27, Column: 33, Byte: 369}, + Start: SourcePos{Line: 28, Column: 26, Byte: 444}, + End: SourcePos{Line: 28, Column: 33, Byte: 451}, }, }, { @@ -423,8 +444,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 26, Column: 1, Byte: 314}, - End: SourcePos{Line: 26, Column: 13, Byte: 326}, + Start: SourcePos{Line: 27, Column: 1, Byte: 396}, + End: SourcePos{Line: 27, Column: 13, Byte: 408}, }, }, { @@ -440,8 +461,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 27, Column: 1, Byte: 337}, - End: SourcePos{Line: 27, Column: 13, Byte: 349}, + Start: SourcePos{Line: 28, Column: 1, Byte: 419}, + End: SourcePos{Line: 28, Column: 13, Byte: 431}, }, }, { @@ -456,8 +477,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 29, Column: 13, Byte: 396}, - End: SourcePos{Line: 29, Column: 16, Byte: 399}, + Start: SourcePos{Line: 30, Column: 12, Byte: 477}, + End: SourcePos{Line: 30, Column: 17, Byte: 482}, }, }, { @@ -472,8 +493,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 30, Column: 13, Byte: 413}, - End: SourcePos{Line: 30, Column: 16, Byte: 416}, + Start: SourcePos{Line: 31, Column: 12, Byte: 494}, + End: SourcePos{Line: 31, Column: 17, Byte: 499}, }, }, { @@ -488,8 +509,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 28, Column: 1, Byte: 371}, - End: SourcePos{Line: 28, Column: 9, Byte: 379}, + Start: SourcePos{Line: 29, Column: 1, Byte: 453}, + End: SourcePos{Line: 29, Column: 9, Byte: 461}, }, }, { @@ -503,8 +524,8 @@ simple_attr = "val" ), &SourceRange{ Filename: "test.tf", - Start: SourcePos{Line: 32, Column: 15, Byte: 434}, - End: SourcePos{Line: 32, Column: 20, Byte: 439}, + Start: SourcePos{Line: 33, Column: 15, Byte: 516}, + End: SourcePos{Line: 33, Column: 20, Byte: 521}, }, }, { diff --git a/internal/tfdiags/diagnostic_extra.go b/internal/tfdiags/diagnostic_extra.go index 2bf25af83051..6294503af370 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 { + DeprecatedOriginDescription() string +} + +// DiagnosticDeprecationOrigin returns the origin range of a deprecation +// warning diagnostic, or nil if the diagnostic does not have such information. +func DeprecatedOriginDescription(diag Diagnostic) string { + maybe := ExtraInfo[DiagnosticExtraDeprecationOrigin](diag) + if maybe == nil { + return "" + } + return maybe.DeprecatedOriginDescription() +} + +type DeprecationOriginDiagnosticExtra struct { + OriginDescription string + + 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) DeprecatedOriginDescription() string { + return c.OriginDescription +}