Adding an analyzer

This walks through adding a new check end to end. Read How tfsprout works first — the two-layer architecture determines most of what you write.

Before you start

Pick the right category and the next free number. Prefixes are AT, R, S, V, with an X prefix for extra checks. Take the next unused number in that range; never reuse a removed ID.

Decide standard or extra. Standard checks flag things that are wrong. Extra checks flag things that are absent or debatable, and go in xpasses/. The test is whether a reasonable provider author could disagree with the finding. See Standard vs extra checks.

Check whether the information already exists. Look through passes/helper/... before writing any AST traversal. If an analyzer already collects the construct you care about, your check is a filter over its results and should contain almost no traversal of its own.

1. Create the package

Create passes/S900/ (or xpasses/XS900/ for an extra check), containing S900.go:

S900 is a placeholder ID used only for this walkthrough, so it never collides with a real check. Use the next free number in the range for your own.

// Package S900 defines an Analyzer that checks for
// Schema that configure both Sensitive and Computed
package S900

import (
    "golang.org/x/tools/go/analysis"

    "github.com/jfrappier/tfsprout/helper/terraformtype/helper/schema"
    "github.com/jfrappier/tfsprout/passes/commentignore"
    "github.com/jfrappier/tfsprout/passes/helper/schema/schemainfo"
)

const Doc = `check for Schema that configure both Sensitive and Computed

The S900 analyzer reports cases of schemas which configure both Sensitive
and Computed, where the sensitivity has no effect.`

const analyzerName = "S900"

var Analyzer = &analysis.Analyzer{
    Name: analyzerName,
    Doc:  Doc,
    Requires: []*analysis.Analyzer{
        schemainfo.Analyzer,
        commentignore.Analyzer,
    },
    Run: run,
}

Conventions that matter:

2. Write the run function

Read your dependencies out of pass.ResultOf, skip suppressed nodes, and report:

func run(pass *analysis.Pass) (interface{}, error) {
    ignorer := pass.ResultOf[commentignore.Analyzer].(*commentignore.Ignorer)
    schemaInfos := pass.ResultOf[schemainfo.Analyzer].([]*schema.SchemaInfo)

    for _, schemaInfo := range schemaInfos {
        if ignorer.ShouldIgnore(analyzerName, schemaInfo.AstCompositeLit) {
            continue
        }

        if !schemaInfo.DeclaresField(schema.SchemaFieldSensitive) || !schemaInfo.DeclaresField(schema.SchemaFieldComputed) {
            continue
        }

        pass.Reportf(schemaInfo.AstCompositeLit.Lbrace, "%s: schema should not configure Sensitive with Computed", analyzerName)
    }

    return nil, nil
}

Three things are non-negotiable:

3. Register it

Add the import and the analyzer to AllChecks in passes/checks.go (or xpasses/checks.go). Both lists are alphabetically ordered — keep them that way.

Only add checks that report. Information-gathering analyzers stay out of AllChecks; they are pulled in automatically through Requires.

TestValidateAllChecks in passes/checks_test.go calls analysis.Validate over the list and will catch a malformed analyzer or a dependency cycle.

4. Add test data

Create testdata/ inside your check's directory. Each check's testdata is its own Go module with a real terraform-plugin-sdk dependency, so the analyzer resolves genuine SDK types rather than stubs:

passes/S900/testdata/
├── go.mod
├── go.sum
└── src/
    └── a/
        └── main_v2.go

Copy go.mod and go.sum from a neighbouring check. In the source files, mark each expected finding with a // want comment carrying a regular expression matched against the report message:

_ = schema.Schema{
    Computed:  true,
    Sensitive: true,
} // want "schema should not configure Sensitive with Computed"

Cover the passing cases too — a check that only has failing fixtures will not catch false positives. Include a file exercising //lintignore:S900 to prove suppression works.

5. Write the test

package S900_test

import (
    "testing"

    "golang.org/x/tools/go/analysis/analysistest"

    "github.com/jfrappier/tfsprout/passes/S900"
)

func TestS900(t *testing.T) {
    testdata := analysistest.TestData()
    analysistest.Run(t, testdata, S900.Analyzer, "testdata/src/a")
}

See Testing for per-check flags, suggested fixes, and golden files.

6. Document it

Add README.md to the check directory, following the structure every other check uses:

# S900

The S900 analyzer reports cases of schemas which configure both
Sensitive and Computed, where the sensitivity has no effect.

## Flagged Code

```go
&schema.Schema{
    Computed:  true,
    Sensitive: true,
}
```

## Passing Code

```go
&schema.Schema{
    Computed: true,
}
```

## Ignoring Reports

Singular reports can be ignored by adding the a `//lintignore:S900` Go code
comment at the end of the offending line or on the line immediately
proceding, e.g.

```go
//lintignore:S900
&schema.Schema{
    Computed:  true,
    Sensitive: true,
}
```

Then add a row to the appropriate table in docs/reference/checks.md, using the first line of your Doc string as the description. CI fails if a check directory has no corresponding row.

7. Verify

go test ./...
go install ./cmd/tfsprout
cd /path/to/a/real/provider && tfsprout -S900 ./...

Running against a real provider is the step that catches false positives. A check that fires on idiomatic code in a well-maintained provider needs narrowing before it ships.

Adding an information-gathering analyzer

If no existing analyzer surfaces what you need, add one under passes/helper/... mirroring the SDK package structure. These analyzers set a ResultType, return data, and never report.

Before writing one by hand, check helper/analysisutils/SelectorExprAnalyzer, FunctionCallExprAnalyzer, ReceiverMethodCallExprAnalyzer and friends construct a complete analyzer from a package path and a name, and cover most cases in a single line.

Model any new SDK type in helper/terraformtype/, alongside the existing type models and package path constants.