lint: Include more source context in unused attribute diagnostics

From some more practical testing of this I realized that usually the first
thing I want to know after seeing this warning is what the object literal
was being assigned to and what else was also defined inside it, and so
this sets the diagnostic's "context" to include the whole containing
object literal so that the source snippet in the diagnostic message is more
immediately useful, without having to cross-reference to the source code
in a separate text editor.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-09-22 09:34:51 -07:00
parent 0fb0a4b707
commit 9895f54ed0
5 changed files with 89 additions and 7 deletions

View File

@@ -265,6 +265,7 @@ func lintVariableDefaultValue(expr hcl.Expression, targetTy cty.Type) hcl.Diagno
nounPhrase, attrName, suggestion,
),
Subject: unused.NameRange.ToHCL().Ptr(),
Context: unused.ContextRange.ToHCL().Ptr(),
})
}

View File

@@ -41,6 +41,13 @@ type DiscardedObjectConstructorAttr struct {
// definition that is being reported.
NameRange tfdiags.SourceRange
// ContextRange is the source range of relevant context surrounding the
// problematic attribute defition, intended to be used for the "Context"
// field of a generated diagnostic so that its included source code
// snippet will also include some surrounding lines that should indicate
// what else was set in the relevant object constructor.
ContextRange tfdiags.SourceRange
// TargetType is the leaf object type that the affected object constructor
// was building a value for. This is the type that the last element of
// Path would traverse into, and which (by definition) does not have an
@@ -131,9 +138,10 @@ func yieldDiscardedObjectConstructorAttrsObject(expr hcl.Expression, targetTy ct
copy(retPath, path)
retPath = append(retPath, cty.GetAttrStep{Name: keyStr})
result := DiscardedObjectConstructorAttr{
Path: retPath,
NameRange: tfdiags.SourceRangeFromHCL(item.KeyExpr.Range()),
TargetType: targetTy,
Path: retPath,
NameRange: tfdiags.SourceRangeFromHCL(item.KeyExpr.Range()),
ContextRange: tfdiags.SourceRangeFromHCL(expr.SrcRange), // the entire containing object literal
TargetType: targetTy,
}
if !yield(result) {
return false

View File

@@ -38,6 +38,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 5, Byte: 6},
End: tfdiags.SourcePos{Line: 2, Column: 8, Byte: 9},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 3, Column: 5, Byte: 22},
},
TargetType: cty.EmptyObject,
},
},
@@ -56,6 +60,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 5, Byte: 6},
End: tfdiags.SourcePos{Line: 2, Column: 8, Byte: 9},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 3, Column: 5, Byte: 22},
},
TargetType: cty.Object(map[string]cty.Type{
"bar": cty.String,
}),
@@ -90,6 +98,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 6, Byte: 7},
End: tfdiags.SourcePos{Line: 2, Column: 9, Byte: 10},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 5, Byte: 6},
End: tfdiags.SourcePos{Line: 2, Column: 18, Byte: 19},
},
TargetType: cty.EmptyObject,
},
{
@@ -98,6 +110,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 26},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 29},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 3, Column: 5, Byte: 25},
End: tfdiags.SourcePos{Line: 3, Column: 19, Byte: 39},
},
TargetType: cty.EmptyObject,
},
},
@@ -116,6 +132,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 27},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 30},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 19, Byte: 20},
End: tfdiags.SourcePos{Line: 4, Column: 6, Byte: 44},
},
TargetType: cty.EmptyObject,
},
},
@@ -145,6 +165,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 6, Byte: 7},
End: tfdiags.SourcePos{Line: 2, Column: 9, Byte: 10},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 5, Byte: 6},
End: tfdiags.SourcePos{Line: 2, Column: 18, Byte: 19},
},
TargetType: cty.EmptyObject,
},
{
@@ -153,6 +177,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 26},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 29},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 3, Column: 5, Byte: 25},
End: tfdiags.SourcePos{Line: 3, Column: 19, Byte: 39},
},
TargetType: cty.EmptyObject,
},
},
@@ -171,6 +199,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 27},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 30},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 19, Byte: 20},
End: tfdiags.SourcePos{Line: 4, Column: 6, Byte: 44},
},
TargetType: cty.EmptyObject,
},
},
@@ -200,6 +232,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 10, Byte: 11},
End: tfdiags.SourcePos{Line: 2, Column: 13, Byte: 14},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 9, Byte: 10},
End: tfdiags.SourcePos{Line: 2, Column: 22, Byte: 23},
},
TargetType: cty.EmptyObject,
},
{
@@ -208,6 +244,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 10, Byte: 34},
End: tfdiags.SourcePos{Line: 3, Column: 13, Byte: 37},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 33},
End: tfdiags.SourcePos{Line: 3, Column: 23, Byte: 47},
},
TargetType: cty.EmptyObject,
},
},
@@ -226,6 +266,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 32},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 35},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 24, Byte: 25},
End: tfdiags.SourcePos{Line: 4, Column: 6, Byte: 49},
},
TargetType: cty.EmptyObject,
},
},
@@ -258,6 +302,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 2, Column: 6, Byte: 7},
End: tfdiags.SourcePos{Line: 2, Column: 9, Byte: 10},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 5, Byte: 6},
End: tfdiags.SourcePos{Line: 2, Column: 18, Byte: 19},
},
TargetType: cty.EmptyObject,
},
{
@@ -266,6 +314,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 26},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 29},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 3, Column: 5, Byte: 25},
End: tfdiags.SourcePos{Line: 3, Column: 19, Byte: 39},
},
TargetType: cty.Object(map[string]cty.Type{"not_baz": cty.String}),
},
},
@@ -311,6 +363,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 3, Column: 6, Byte: 17},
End: tfdiags.SourcePos{Line: 3, Column: 9, Byte: 20},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 2, Column: 9, Byte: 10},
End: tfdiags.SourcePos{Line: 4, Column: 6, Byte: 34},
},
TargetType: cty.EmptyObject,
},
{
@@ -319,6 +375,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 6, Column: 6, Byte: 50},
End: tfdiags.SourcePos{Line: 6, Column: 9, Byte: 53},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 5, Column: 9, Byte: 43},
End: tfdiags.SourcePos{Line: 7, Column: 6, Byte: 68},
},
TargetType: cty.Object(map[string]cty.Type{"not_baz": cty.String}),
},
{
@@ -327,6 +387,10 @@ func TestDiscardedObjectConstructorAttrs(t *testing.T) {
Start: tfdiags.SourcePos{Line: 8, Column: 5, Byte: 73},
End: tfdiags.SourcePos{Line: 8, Column: 6, Byte: 74},
},
ContextRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 9, Column: 5, Byte: 84},
},
TargetType: cty.Object(map[string]cty.Type{
"a": cty.EmptyObject,
"b": cty.Object(map[string]cty.Type{"not_baz": cty.String}),

View File

@@ -292,6 +292,7 @@ func (n *nodeModuleVariable) warningDiags() tfdiags.Diagnostics {
n.Addr.Variable.Name, extraPathClause, attrName, suggestion,
),
Subject: unused.NameRange.ToHCL().Ptr(),
Context: unused.ContextRange.ToHCL().Ptr(),
})
}

View File

@@ -322,12 +322,13 @@ func TestNodeModuleVariable_warningDiags(t *testing.T) {
}),
},
Expr: &hclsyntax.ObjectConsExpr{
SrcRange: hcl.Range{Filename: "context1.tofu"},
Items: []hclsyntax.ObjectConsItem{
{
KeyExpr: &hclsyntax.LiteralValueExpr{
Val: cty.StringVal("baz"),
SrcRange: hcl.Range{
Filename: "test.tofu",
Filename: "test1.tofu",
},
},
ValueExpr: &hclsyntax.LiteralValueExpr{
@@ -342,12 +343,13 @@ func TestNodeModuleVariable_warningDiags(t *testing.T) {
},
},
ValueExpr: &hclsyntax.ObjectConsExpr{
SrcRange: hcl.Range{Filename: "context2.tofu"},
Items: []hclsyntax.ObjectConsItem{
{
KeyExpr: &hclsyntax.LiteralValueExpr{
Val: cty.StringVal("beep"),
SrcRange: hcl.Range{
Filename: "test.tofu",
Filename: "test2.tofu",
},
},
ValueExpr: &hclsyntax.LiteralValueExpr{
@@ -371,7 +373,10 @@ func TestNodeModuleVariable_warningDiags(t *testing.T) {
Summary: "Object attribute is ignored",
Detail: `The object type for input variable "foo" does not include an attribute named "baz", so this definition is unused. Did you mean to set attribute "bar" instead?`,
Subject: &hcl.Range{
Filename: "test.tofu",
Filename: "test1.tofu", // from synthetic source range in constructed expression above
},
Context: &hcl.Range{
Filename: "context1.tofu", // from synthetic source range in constructed expression above
},
})
wantDiags = wantDiags.Append(&hcl.Diagnostic{
@@ -379,7 +384,10 @@ func TestNodeModuleVariable_warningDiags(t *testing.T) {
Summary: "Object attribute is ignored",
Detail: `The object type for input variable "foo" nested value .bar does not include an attribute named "beep", so this definition is unused.`,
Subject: &hcl.Range{
Filename: "test.tofu",
Filename: "test2.tofu", // from synthetic source range in constructed expression above
},
Context: &hcl.Range{
Filename: "context2.tofu", // from synthetic source range in constructed expression above
},
})
wantDiags = wantDiags.ForRPC()