Lint rules for the Terraform AzureRM provider. Each rule catches one mistake that comes up in provider code, from schema fields that break at plan time to pointer dereferences that panic when Azure leaves a field out. Most rules can fix what they find.
It runs on its own or as a golangci-lint plugin. The rules are ordinary Go analysis passes.
go install github.com/katbyte/azproviderlint@latest
azproviderlint ./...That runs every rule. To run some of them, name them. A category on its own (-AZG) means every rule in it.
azproviderlint -AZG001 ./...
azproviderlint -AZR001 -AZR003 ./...
azproviderlint -AZG ./...
azproviderlint -AZG -AZR001 ./...Add -fix to apply the suggested fixes. Read the diff afterwards. Each rule's README says what its fix does and when to be careful.
Rules are named AZ, a category letter, and a number. The letters follow tfproviderlint where the categories overlap.
| Rule | Description |
|---|---|
| AZG000 | azignore directives must give a reason |
| AZG001 | combine err assignment and check into one if |
| AZG002 | use new() instead of a single-use temporary's address |
| AZG003 | use pointer.ToEnum for enum conversions |
| AZG004 | use pointer.From instead of nil-check dereference |
| AZG005 | inline single-use variable only used in a later assignment or return |
| AZG006 | inline single-use variable only used in a later function call |
| AZG007 | omit struct literal fields explicitly set to their zero value |
| AZG008 | pointer dereferences must have a nil guard or use pointer.From |
| AZG009 | use pointer.FromEnum for enum conversions |
| Rule | Description |
|---|---|
| AZP001 | Microsoft docs URLs must not carry a locale segment like /en-us/ |
| AZP002 | registered resources must have a same-named data source |
| AZP003 | data sources must expose their same-named resource's properties |
| AZP004 | registration entries must be sorted alphabetically |
| AZP005 | do not set the case-insensitive segments feature flag |
| Rule | Description |
|---|---|
| AZR001 | SetId must use a resource id formatter/parser |
| AZR002 | separate Create and Update methods |
| AZR003 | no d.Get in Delete functions |
| AZR004 | compare resource id types with resourceids.Match |
| AZR006 | use timeouts wrappers, not StopContext |
| AZR007 | use custom pollers instead of StateChangeConf |
| AZR008 | flatten functions must return empty slices/maps, not nil |
| AZR009 | no lifecycle narration logging in Create/Read/Update/Delete |
| AZR010 | flatten functions handle nil input themselves, not their callers |
| Rule | Description |
|---|---|
| AZD001 | data sources must error when not found, not SetId("") |
| AZD002 | data sources must error when not found, not MarkAsGone |
| Rule | Description |
|---|---|
| AZS001 | typed SDK model numeric fields must be 64-bit (int64/float64) |
| AZS002 | schema Default values must match the declared Type |
| AZS003 | TypeList blocks must not allow empty blocks |
| AZS004 | enum validation must use the SDK's possible-values helper |
| AZS007 | optional+computed fields must have a Note: O+C comment |
| AZS009 | computed-only fields must not set input-only schema attributes |
| Rule | Description |
|---|---|
| AZC001 | clients must set an explicit resource manager endpoint |
| Rule | Description |
|---|---|
| AZT001 | acceptance tests must use a _test package |
| AZT002 | acceptance tests must not read credentials from the environment |
No rules yet. Reserved for property naming rules, such as percentages using a _percentage suffix rather than _in_percent.
| Rule | Description |
|---|---|
| AZV001 | 'invalid format' error messages must describe the expected format |
If you are new to the provider, these come up a lot:
- Untyped resource: the original plugin SDK style. A
map[string]*pluginsdk.Schema, andd.Get/d.Setto move values in and out of state. - Typed resource: the newer
internal/sdkstyle. A Go struct withtfschematags,Arguments()/Attributes()for the schema, andmetadata.Decode/metadata.Encodeinstead ofGet/Set. - Framework resource: the Terraform Plugin Framework style, registered through
FrameworkResources(). - Expand / flatten:
expandFooturns config into an SDK request.flattenFooturns an SDK response into what goes in state. - Resource ID: the provider's own parsed form of an Azure resource ID, with a generated formatter and parser per type. See AZR001.
- go-azure-sdk / go-azure-helpers: the SDK the provider calls Azure with, and the helper library that provides
pointer.To,pointer.From, and friends.
To skip one rule on one line, add a comment at the end of the line or on the line above it. Say why. A directive without a reason is reported by AZG000.
d.SetId(*read.ID) //azignore:AZR001 - legacy resource, ID formatter tracked in #1234
//azignore:AZG001,AZR003 combined form obscures the retry loop here
err := client.Delete(ctx, id)Several rules can be listed with commas. The - before the reason is optional. This works under every driver, standalone or golangci-lint.
Under golangci-lint, //nolint:azproviderlint in the same place also works, but it silences every azproviderlint rule on that line, so prefer //azignore when you can.
The plugin has to be compiled into a custom golangci-lint binary. Add it to .custom-gcl.yml:
version: v2.12.2
plugins:
- module: "github.com/katbyte/azproviderlint"
import: "github.com/katbyte/azproviderlint/plugin"
version: v0.1.0Build the binary and enable the linter:
golangci-lint customlinters:
enable:
- azproviderlint
settings:
custom:
azproviderlint:
type: moduleTo run only azproviderlint through the custom binary:
custom-gcl run --enable-only azproviderlint ./...Every rule runs by default. Under settings, enable narrows that to a list and disable removes from whatever is enabled. Both accept rule names or whole categories.
linters:
settings:
custom:
azproviderlint:
type: module
settings:
enable: [AZG] # only the AZG rules...
disable: [AZG005] # ...except AZG005There is no per-rule flag on the golangci-lint command line. Use the settings above, or the standalone binary.
Some rules take options. Each rule's README lists them. In golangci-lint they go under the rule's name in the same settings block. On the standalone binary they are -<RULE>.<option> flags.
settings:
AZS004: {allow-extra-values: true}
AZG005: {max-gap: 50}azproviderlint -AZG005 -AZG005.max-gap=50 ./...Building a custom binary is a one-time cost, and on a codebase the size of azurerm it pays off:
- Loading the provider once, not twice. Type-checking azurerm and its vendor tree takes minutes. A separate binary does it all over again. Inside golangci-lint it is shared, and the result cache makes warm re-runs fast.
- One config, one output. The path exclusions already in the provider's
.golangci.yml(generated files,/sdk/,third_party) apply for free. So do SARIF, annotations, and--new-from-rev, which lets a rule be enforced on new code while a decade of existing findings is left alone. //nolintworks alongside//azignore.- One pass for every provider linter. tfproviderlint-golangci wraps tfproviderlint as a module plugin too, so a custom binary can carry golangci-lint, tfproviderlint, and azproviderlint together: three tools doing three passes become one.
The standalone binary is still the right tool for a quick one-rule run, for editors that expect a plain analysis vet tool, and for developing new rules.