Skip to content

Commit

Permalink
Implement unreferenced variable validation (#1357)
Browse files Browse the repository at this point in the history
Add the ability to use the collected origin and target references in early validation by providing a hook for validation funcs. This also adds a validator for unreferenced variables.

Validation funcs will be provided by terraform-ls for now, but may be moved into hcl-lang in the future.
  • Loading branch information
jpogran authored and dbanck committed Aug 22, 2023
1 parent 91febd3 commit 7efe5b2
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hc-install v0.5.2
github.com/hashicorp/hcl-lang v0.0.0-20230616080040-23442190b6b7
github.com/hashicorp/hcl-lang v0.0.0-20230816183022-371318075931
github.com/hashicorp/hcl/v2 v2.17.0
github.com/hashicorp/terraform-exec v0.18.1
github.com/hashicorp/terraform-json v0.17.1
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
Expand All @@ -217,8 +218,13 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl-lang v0.0.0-20230616080040-23442190b6b7 h1:tKk3jagIGu+0q/iUDLefzUfUmH0Txk+Cr0QXme33cBk=
github.com/hashicorp/hcl-lang v0.0.0-20230616080040-23442190b6b7/go.mod h1:Y5G573YDt5m5HDSPMN3awPrpUjRLK6LucNYcd90RTI4=
github.com/hashicorp/hcl-lang v0.0.0-20230810124751-5753e8194816 h1:mJTyK9+yMLptuwhJba1Chnn5OSUtxo5FsQ++6xDipTs=
github.com/hashicorp/hcl-lang v0.0.0-20230810124751-5753e8194816/go.mod h1:xlYq00R/OYgpAmScjO1zkE9NCjl6zuMex1VVcUU0pjQ=
github.com/hashicorp/hcl-lang v0.0.0-20230816183022-371318075931 h1:l9TXEz7RJ6eOiXDQovcP0dZ6gruXQi/stbJYtpEKQII=
github.com/hashicorp/hcl-lang v0.0.0-20230816183022-371318075931/go.mod h1:xlYq00R/OYgpAmScjO1zkE9NCjl6zuMex1VVcUU0pjQ=
github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY=
github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4=
github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980=
github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA=
Expand Down Expand Up @@ -333,6 +339,7 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
Expand Down
7 changes: 7 additions & 0 deletions internal/decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"context"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-ls/internal/codelens"
"github.com/hashicorp/terraform-ls/internal/decoder/validations"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
"github.com/hashicorp/terraform-ls/internal/state"
Expand Down Expand Up @@ -91,5 +93,10 @@ func DecoderContext(ctx context.Context) decoder.DecoderContext {
}
}

validations := []lang.ValidationFunc{
validations.UnreferencedOrigins,
}
dCtx.Validations = append(dCtx.Validations, validations...)

return dCtx
}
60 changes: 60 additions & 0 deletions internal/decoder/validations/unreferenced_origin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validations

import (
"context"
"fmt"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
)

func UnreferencedOrigins(ctx context.Context) lang.DiagnosticsMap {
diagsMap := make(lang.DiagnosticsMap)

pathCtx, err := decoder.PathCtx(ctx)
if err != nil {
return diagsMap
}

for _, origin := range pathCtx.ReferenceOrigins {
matchableOrigin, ok := origin.(reference.MatchableOrigin)
if !ok {
// we don't report on other origins to avoid complexity for now
// other origins would need to be matched against other
// modules/directories and we cannot be sure the targets are
// available within the workspace or were parsed/decoded/collected
// at the time this event occurs
continue
}

// we only initially validate variables
// resources and data sources can have unknown schema
// and will be researched at a later point
firstStep := matchableOrigin.Address()[0]
if firstStep.String() != "var" {
continue
}

_, ok = pathCtx.ReferenceTargets.Match(matchableOrigin)
if !ok {
// target not found
fileName := origin.OriginRange().Filename
d := &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("No declaration found for %q", matchableOrigin.Address()),
Subject: origin.OriginRange().Ptr(),
}
diagsMap[fileName] = diagsMap[fileName].Append(d)

continue
}

}

return diagsMap
}
120 changes: 120 additions & 0 deletions internal/decoder/validations/unreferenced_origin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validations

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
)

func TestUnreferencedOrigins(t *testing.T) {
tests := []struct {
name string
origins reference.Origins
want lang.DiagnosticsMap
}{
{
name: "undeclared variable",
origins: reference.Origins{
reference.LocalOrigin{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{},
End: hcl.Pos{},
},
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "foo"},
},
},
},
want: lang.DiagnosticsMap{
"test.tf": hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No declaration found for \"var.foo\"",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{},
End: hcl.Pos{},
},
},
},
},
},
{
name: "many undeclared variables",
origins: reference.Origins{
reference.LocalOrigin{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 10, Byte: 10},
},
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "foo"},
},
},
reference.LocalOrigin{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 10, Byte: 10},
},
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "wakka"},
},
},
},
want: lang.DiagnosticsMap{
"test.tf": hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No declaration found for \"var.foo\"",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 10, Byte: 10},
},
},
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No declaration found for \"var.wakka\"",
Subject: &hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 0},
End: hcl.Pos{Line: 2, Column: 10, Byte: 10},
},
},
},
},
},
}

for i, tt := range tests {
t.Run(fmt.Sprintf("%2d-%s", i, tt.name), func(t *testing.T) {
ctx := context.Background()

pathCtx := &decoder.PathContext{
ReferenceOrigins: tt.origins,
}

ctx = decoder.WithPathContext(ctx, pathCtx)

diags := UnreferencedOrigins(ctx)
if diff := cmp.Diff(tt.want["test.tf"], diags["test.tf"]); diff != "" {
t.Fatalf("unexpected diagnostics: %s", diff)
}
})
}
}

0 comments on commit 7efe5b2

Please sign in to comment.