diff --git a/internal/command/init.go b/internal/command/init.go index e7667cd6f6..ad59f6c463 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -292,8 +292,7 @@ func (c *InitCommand) Run(args []string) int { // Now, we can check the diagnostics from the early configuration and the // backend. - diags = diags.Append(earlyConfDiags) - diags = diags.Append(backDiags) + diags = diags.Append(earlyConfDiags.StrictDeduplicateMerge(backDiags)) if earlyConfDiags.HasErrors() { c.Ui.Error(strings.TrimSpace(errInitConfigError)) c.showDiagnostics(diags) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 27faaef2b5..677f33ff05 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -2995,6 +2995,33 @@ func TestInit_moduleVersion(t *testing.T) { }) } +func TestInit_invalidExtraLabel(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-syntax-invalid-extra-label"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, _ := testView(t) + m := Meta{ + Ui: ui, + View: view, + } + + c := &InitCommand{ + Meta: m, + } + + if code := c.Run(nil); code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + } + + errStr := ui.ErrorWriter.String() + splitted := strings.Split(errStr, "Error: Unsupported block type") + if len(splitted) != 2 { + t.Fatalf("want exactly one unsupported block type errors but got: %d\nstderr:\n%s\n\nstdout:\n%s", len(splitted)-1, errStr, ui.OutputWriter.String()) + } +} + // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/command/testdata/init-syntax-invalid-extra-label/main.tf b/internal/command/testdata/init-syntax-invalid-extra-label/main.tf new file mode 100644 index 0000000000..1cfa82d325 --- /dev/null +++ b/internal/command/testdata/init-syntax-invalid-extra-label/main.tf @@ -0,0 +1,10 @@ +terraform { + extra_label{ + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0.2" + } + } + } +} \ No newline at end of file diff --git a/internal/tfdiags/diagnostic.go b/internal/tfdiags/diagnostic.go index 4780f7e6d3..564eb4b4ed 100644 --- a/internal/tfdiags/diagnostic.go +++ b/internal/tfdiags/diagnostic.go @@ -58,11 +58,19 @@ type Description struct { Detail string } +func (d Description) Equal(other Description) bool { + return d.Address == other.Address && d.Summary == other.Summary && d.Detail == other.Detail +} + type Source struct { Subject *SourceRange Context *SourceRange } +func (s Source) Equal(other Source) bool { + return s.Subject.Equal(other.Subject) && s.Context.Equal(other.Context) +} + type FromExpr struct { Expression hcl.Expression EvalContext *hcl.EvalContext diff --git a/internal/tfdiags/diagnostics.go b/internal/tfdiags/diagnostics.go index 4c2331b062..0a60b2bcba 100644 --- a/internal/tfdiags/diagnostics.go +++ b/internal/tfdiags/diagnostics.go @@ -101,6 +101,23 @@ func (diags Diagnostics) Append(new ...interface{}) Diagnostics { return diags } +func (diags Diagnostics) StrictDeduplicateMerge(other Diagnostics) Diagnostics { + if len(diags) == 0 { + return other + } + + for _, d := range diags { + for _, o := range other { + isEqual := d.Description().Equal(o.Description()) && d.Severity() == o.Severity() && d.Source().Equal(o.Source()) + if DoNotConsolidateDiagnostic(d) || DoNotConsolidateDiagnostic(o) || !isEqual { + diags = append(diags, o) + } + } + } + + return diags +} + // HasErrors returns true if any of the diagnostics in the list have // a severity of Error. func (diags Diagnostics) HasErrors() bool { diff --git a/internal/tfdiags/source_range.go b/internal/tfdiags/source_range.go index 955efcab4f..8b86efee56 100644 --- a/internal/tfdiags/source_range.go +++ b/internal/tfdiags/source_range.go @@ -16,10 +16,22 @@ type SourceRange struct { Start, End SourcePos } +func (r *SourceRange) Equal(other *SourceRange) bool { + if r == nil || other == nil { + return r == other + } + + return r.Filename == other.Filename && r.Start.Equal(other.Start) && r.End.Equal(other.End) +} + type SourcePos struct { Line, Column, Byte int } +func (p SourcePos) Equal(other SourcePos) bool { + return p.Line == other.Line && p.Column == other.Column && p.Byte == other.Byte +} + // StartString returns a string representation of the start of the range, // including the filename and the line and column numbers. func (r SourceRange) StartString() string {