Skip to content

Commit 92926ff

Browse files
authored
New Checks for common unstable d.SetId() values and introduce standard library package helpers (#193)
* Introduce standard library helpers and pass for fmt.Sprintf() calls Functionality to replace current Terraform AWS Provider handling and generally will be useful in the future. * passes: New Checks for common unstable d.SetId() values Reference: #191 Reference: #192
1 parent e45dbaf commit 92926ff

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+912
-0
lines changed

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# v0.17.0
2+
3+
FEATURES
4+
5+
* **New Check:** `R015`: check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `resource.UniqueId()` value
6+
* **New Check:** `R016`: check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `resource.PrefixedUniqueId()` value
7+
* **New Check:** `R017`: check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `time.Now()` value
8+
9+
ENHANCEMENTS
10+
11+
* helper/analysisutils: Add `StdlibFunctionCallExprAnalyzer` with associated runner
12+
* helper/astutils: Support standard library handling equivalents (no vendoring) for package functions
13+
* passes/helper/schema: Pass for collecting `(*schema.ResourceData).SetId()` calls
14+
* passes/stdlib: Pass for collecting `fmt.Sprintf()` calls
15+
116
# v0.16.0
217

318
BREAKING CHANGES

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ Standard lint checks are enabled by default in the `tfproviderlint` tool. Opt-in
109109
| [R012](passes/R012/README.md) | check for data source `Resource` that configure `CustomizeDiff` | AST |
110110
| [R013](passes/R013/README.md) | check for `map[string]*Resource` that resource names contain at least one underscore | AST |
111111
| [R014](passes/R014/README.md) | check for `CreateFunc`, `DeleteFunc`, `ReadFunc`, and `UpdateFunc` parameter naming | AST |
112+
| [R015](passes/R015/README.md) | check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `resource.UniqueId()` value | AST |
113+
| [R016](passes/R016/README.md) | check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `resource.PrefixedUniqueId()` value | AST |
114+
| [R017](passes/R017/README.md) | check for `(*schema.ResourceData).SetId()` receiver method usage with unstable `time.Now()` value | AST |
112115

113116
### Standard Schema Checks
114117

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package analysisutils
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"reflect"
7+
8+
"golang.org/x/tools/go/analysis"
9+
"golang.org/x/tools/go/analysis/passes/inspect"
10+
)
11+
12+
// StdlibFunctionCallExprAnalyzer returns an Analyzer for standard library function *ast.CallExpr
13+
func StdlibFunctionCallExprAnalyzer(analyzerName string, packagePath string, functionName string) *analysis.Analyzer {
14+
return &analysis.Analyzer{
15+
Name: analyzerName,
16+
Doc: fmt.Sprintf("find %s.%s() calls for later passes", packagePath, functionName),
17+
Requires: []*analysis.Analyzer{
18+
inspect.Analyzer,
19+
},
20+
Run: StdlibFunctionCallExprRunner(packagePath, functionName),
21+
ResultType: reflect.TypeOf([]*ast.CallExpr{}),
22+
}
23+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package analysisutils
2+
3+
import (
4+
"go/ast"
5+
6+
"github.com/bflad/tfproviderlint/helper/astutils"
7+
"golang.org/x/tools/go/analysis"
8+
"golang.org/x/tools/go/analysis/passes/inspect"
9+
"golang.org/x/tools/go/ast/inspector"
10+
)
11+
12+
// StdlibFunctionCallExprRunner returns an Analyzer runner for function *ast.CallExpr
13+
func StdlibFunctionCallExprRunner(packagePath string, functionName string) func(*analysis.Pass) (interface{}, error) {
14+
return func(pass *analysis.Pass) (interface{}, error) {
15+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
16+
nodeFilter := []ast.Node{
17+
(*ast.CallExpr)(nil),
18+
}
19+
var result []*ast.CallExpr
20+
21+
inspect.Preorder(nodeFilter, func(n ast.Node) {
22+
callExpr := n.(*ast.CallExpr)
23+
24+
if !astutils.IsStdlibPackageFunc(callExpr.Fun, pass.TypesInfo, packagePath, functionName) {
25+
return
26+
}
27+
28+
result = append(result, callExpr)
29+
})
30+
31+
return result, nil
32+
}
33+
}

helper/astutils/package.go

+88
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,94 @@ func IsPackageType(t types.Type, packageSuffix string, typeName string) bool {
185185
return false
186186
}
187187

188+
// IsStdlibPackageReceiverMethod returns true if the package suffix (for vendoring), receiver name, and method name match
189+
//
190+
// This function checks an explicit import path without vendoring. To allow
191+
// vendored paths, use IsPackageReceiverMethod instead.
192+
func IsStdlibPackageReceiverMethod(e ast.Expr, info *types.Info, packagePath string, receiverName, methodName string) bool {
193+
switch e := e.(type) {
194+
case *ast.SelectorExpr:
195+
if e.Sel.Name != methodName {
196+
return false
197+
}
198+
199+
return IsStdlibPackageType(info.TypeOf(e.X), packagePath, receiverName)
200+
}
201+
202+
return false
203+
}
204+
205+
// IsStdlibPackageFunc returns true if the function package suffix (for vendoring) and name matches
206+
//
207+
// This function checks an explicit import path without vendoring. To allow
208+
// vendored paths, use IsPackageFunc instead.
209+
func IsStdlibPackageFunc(e ast.Expr, info *types.Info, packagePath string, funcName string) bool {
210+
switch e := e.(type) {
211+
case *ast.SelectorExpr:
212+
if e.Sel.Name != funcName {
213+
return false
214+
}
215+
216+
switch x := e.X.(type) {
217+
case *ast.Ident:
218+
return info.ObjectOf(x).(*types.PkgName).Imported().Path() == packagePath
219+
}
220+
case *ast.StarExpr:
221+
return IsStdlibPackageFunc(e.X, info, packagePath, funcName)
222+
}
223+
224+
return false
225+
}
226+
227+
// IsStdlibPackageFunctionFieldListType returns true if the function parameter package suffix (for vendoring) and name matches
228+
//
229+
// This function checks an explicit import path without vendoring. To allow
230+
// vendored paths, use IsPackageFunctionFieldListType instead.
231+
func IsStdlibPackageFunctionFieldListType(e ast.Expr, info *types.Info, packagePath string, typeName string) bool {
232+
switch e := e.(type) {
233+
case *ast.SelectorExpr:
234+
if e.Sel.Name != typeName {
235+
return false
236+
}
237+
238+
switch x := e.X.(type) {
239+
case *ast.Ident:
240+
return info.ObjectOf(x).(*types.PkgName).Imported().Path() == packagePath
241+
}
242+
case *ast.StarExpr:
243+
return IsStdlibPackageFunctionFieldListType(e.X, info, packagePath, typeName)
244+
}
245+
246+
return false
247+
}
248+
249+
// IsStdlibPackageNamedType returns if the type name matches and is from the package suffix
250+
//
251+
// This function checks an explicit import path without vendoring. To allow
252+
// vendored paths, use IsPackageNamedType instead.
253+
func IsStdlibPackageNamedType(t *types.Named, packagePath string, typeName string) bool {
254+
if t.Obj().Name() != typeName {
255+
return false
256+
}
257+
258+
return t.Obj().Pkg().Path() == packagePath
259+
}
260+
261+
// IsStdlibPackageType returns true if the type name can be matched and is from the package suffix
262+
//
263+
// This function checks an explicit import path without vendoring. To allow
264+
// vendored paths, use IsPackageType instead.
265+
func IsStdlibPackageType(t types.Type, packagePath string, typeName string) bool {
266+
switch t := t.(type) {
267+
case *types.Named:
268+
return IsStdlibPackageNamedType(t, packagePath, typeName)
269+
case *types.Pointer:
270+
return IsStdlibPackageType(t.Elem(), packagePath, typeName)
271+
}
272+
273+
return false
274+
}
275+
188276
func isModulePackagePath(module string, packageSuffix string, path string) bool {
189277
// Only check end of path due to vendoring
190278
r := regexp.MustCompile(fmt.Sprintf("%s(/v[1-9][0-9]*)?/%s$", module, packageSuffix))

helper/terraformtype/helper/resource/funcs.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const (
55
FuncNameComposeTestCheckFunc = `ComposeTestCheckFunc`
66
FuncNameNonRetryableError = `NonRetryableError`
77
FuncNameParallelTest = `ParallelTest`
8+
FuncNamePrefixedUniqueId = `PrefixedUniqueId`
89
FuncNameRetryableError = `RetryableError`
910
FuncNameTest = `Test`
1011
FuncNameTestCheckNoResourceAttr = `TestCheckNoResourceAttr`
@@ -14,4 +15,5 @@ const (
1415
FuncNameTestCheckResourceAttrPtr = `TestCheckResourceAttrPtr`
1516
FuncNameTestCheckResourceAttrSet = `TestCheckResourceAttrSet`
1617
FuncNameTestMatchResourceAttr = `TestMatchResourceAttr`
18+
FuncNameUniqueId = `UniqueId`
1719
)

passes/R015/R015.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package R015
2+
3+
import (
4+
"go/ast"
5+
6+
"golang.org/x/tools/go/analysis"
7+
8+
"github.com/bflad/tfproviderlint/helper/terraformtype/helper/resource"
9+
"github.com/bflad/tfproviderlint/passes/commentignore"
10+
"github.com/bflad/tfproviderlint/passes/helper/schema/resourcedatasetidcallexpr"
11+
)
12+
13+
const Doc = `check for (*schema.ResourceData).SetId() usage with unstable resource.UniqueId() value
14+
15+
Schema attributes should be stable across Terraform runs.`
16+
17+
const analyzerName = "R015"
18+
19+
var Analyzer = &analysis.Analyzer{
20+
Name: analyzerName,
21+
Doc: Doc,
22+
Requires: []*analysis.Analyzer{
23+
commentignore.Analyzer,
24+
resourcedatasetidcallexpr.Analyzer,
25+
},
26+
Run: run,
27+
}
28+
29+
func run(pass *analysis.Pass) (interface{}, error) {
30+
ignorer := pass.ResultOf[commentignore.Analyzer].(*commentignore.Ignorer)
31+
callExprs := pass.ResultOf[resourcedatasetidcallexpr.Analyzer].([]*ast.CallExpr)
32+
for _, callExpr := range callExprs {
33+
if ignorer.ShouldIgnore(analyzerName, callExpr) {
34+
continue
35+
}
36+
37+
if len(callExpr.Args) < 1 {
38+
continue
39+
}
40+
41+
ast.Inspect(callExpr.Args[0], func(n ast.Node) bool {
42+
callExpr, ok := n.(*ast.CallExpr)
43+
44+
if !ok {
45+
return true
46+
}
47+
48+
if resource.IsFunc(callExpr.Fun, pass.TypesInfo, resource.FuncNameUniqueId) {
49+
pass.Reportf(callExpr.Pos(), "%s: schema attributes should be stable across Terraform runs", analyzerName)
50+
return false
51+
}
52+
53+
return true
54+
})
55+
}
56+
57+
return nil, nil
58+
}

passes/R015/R015_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package R015_test
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/tools/go/analysis/analysistest"
7+
8+
"github.com/bflad/tfproviderlint/passes/R015"
9+
)
10+
11+
func TestR015(t *testing.T) {
12+
testdata := analysistest.TestData()
13+
analysistest.Run(t, testdata, R015.Analyzer, "a")
14+
}

passes/R015/README.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# R015
2+
3+
The R015 analyzer reports [`(*schema.ResourceData).SetId()`](https://godoc.org/github.com/hashicorp/terraform-plugin-sdk/helper/schema#ResourceData.Set) usage with unstable `resource.UniqueId()` value. Schema attributes should be stable across Terraform runs.
4+
5+
## Flagged Code
6+
7+
```go
8+
d.SetId(resource.UniqueId())
9+
```
10+
11+
## Passing Code
12+
13+
```go
14+
d.SetId("stablestring")
15+
```
16+
17+
## Ignoring Reports
18+
19+
Singular reports can be ignored by adding the a `//lintignore:R015` Go code comment at the end of the offending line or on the line immediately proceding, e.g.
20+
21+
```go
22+
//lintignore:R015
23+
d.SetId(resource.UniqueId())
24+
```

passes/R015/testdata/src/a/alias.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package a
2+
3+
import (
4+
r "github.com/hashicorp/terraform-plugin-sdk/helper/resource"
5+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
6+
)
7+
8+
func falias() {
9+
var d schema.ResourceData
10+
11+
d.SetId(r.UniqueId()) // want "schema attributes should be stable across Terraform runs"
12+
13+
d.SetId("test")
14+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package a
2+
3+
import (
4+
r "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
5+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
6+
)
7+
8+
func falias_v2() {
9+
var d schema.ResourceData
10+
11+
d.SetId(r.UniqueId()) // want "schema attributes should be stable across Terraform runs"
12+
13+
d.SetId("test")
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package a
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
5+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
6+
)
7+
8+
func fcommentignore() {
9+
var d schema.ResourceData
10+
11+
d.SetId(resource.UniqueId()) //lintignore:R015
12+
13+
//lintignore:R015
14+
d.SetId(resource.UniqueId())
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package a
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
5+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
6+
)
7+
8+
func fcommentignore_v2() {
9+
var d schema.ResourceData
10+
11+
d.SetId(resource.UniqueId()) //lintignore:R015
12+
13+
//lintignore:R015
14+
d.SetId(resource.UniqueId())
15+
}

passes/R015/testdata/src/a/main.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package a
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
5+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
6+
)
7+
8+
func f() {
9+
var d schema.ResourceData
10+
11+
d.SetId(resource.UniqueId()) // want "schema attributes should be stable across Terraform runs"
12+
13+
d.SetId("test")
14+
15+
fResourceFunc(&d, nil)
16+
}
17+
18+
func fResourceFunc(d *schema.ResourceData, meta interface{}) error {
19+
d.SetId(resource.UniqueId()) // want "schema attributes should be stable across Terraform runs"
20+
21+
d.SetId("test")
22+
23+
return nil
24+
}

0 commit comments

Comments
 (0)