Linting in Geppetto (go/analysis and custom vettools)

How Geppetto runs linting, how to add custom go/analysis analyzers, and how turnsdatalint works

Sections

Terminology & Glossary
📖 Documentation
Navigation
54 sectionsv0.1
📄 Linting in Geppetto (go/analysis and custom vettools) — glaze help geppetto-linting
geppetto-linting

Linting in Geppetto (go/analysis and custom vettools)

How Geppetto runs linting, how to add custom go/analysis analyzers, and how turnsdatalint works

Tutorialgeppettolinttoolinggo-analysis

Linting in Geppetto (go/analysis and custom vettools)

Linting is how we encode “team rules” that the Go compiler cannot enforce by itself. In Geppetto we combine standard linters (via golangci-lint) with project-specific checks implemented as go vet plugins using the go/analysis framework. This keeps checks fast, type-aware, and easy to run in CI and locally.

What runs when you type make lint

make lint is the blessed command for contributors. It is expected to:

  • Build the repo (including codegen) so type-based checks see up-to-date code
  • Run golangci-lint for standard Go linting
  • Run a bundled custom vettool (cmd/geppetto-lint) via go vet -vettool=... ./...

For custom analyzers only, use:

  • make linttool: builds and runs the bundled vettool (multichecker)
  • make turnsdatalint: builds and runs the single analyzer tool (singlechecker)

How custom linters are packaged (so downstream repos can reuse them)

Geppetto packages analyzers under pkg/analysis/<name> (not internal/) so third-party projects can import them. We then provide two entrypoints:

  • Single analyzer (debug-friendly): cmd/<name> using singlechecker.Main(<name>.Analyzer)
  • Bundled tool (future-proof): cmd/geppetto-lint using multichecker.Main(...)

This allows downstream repos to either run Geppetto’s bundled tool directly, or embed Geppetto analyzers into their own multichecker alongside their own checks.

How to add a new custom analyzer (checklist)

To add a new analyzer that scales with the bundling approach:

  • Create pkg/analysis/<name>/analyzer.go exporting var Analyzer *analysis.Analyzer
  • Add tests with analysistest under pkg/analysis/<name>/testdata/src/...
  • Register the analyzer in cmd/geppetto-lint/main.go
  • Optionally add cmd/<name>/main.go as a singlechecker wrapper
  • Document it under pkg/doc/topics/
  • Put reference demos under */testdata/reference/ so go vet ./... doesn’t pick them up during normal lint runs

turnsdatalint (key + payload linting)

turnsdatalint is a fast go/analysis-based linter that enforces two conventions:

  • Block payload keys must be const strings (use turns.PayloadKeyText, etc; no raw literals and no variables).
  • Typed key constructors (turns.DataK, turns.TurnMetaK, turns.BlockMetaK) must only be called in key-definition files so the rest of the codebase reuses canonical key variables.

In current Geppetto, Turn.Data, Turn.Metadata, and Block.Metadata are wrapper stores (not map[...]... fields), so direct indexing is not possible; typed-key access is done via key.Get/key.Set. The linter still applies typed-key index rules to any remaining map fields (notably Run.Metadata).

What it enforces

turnsdatalint enforces:

  • Run.Metadata (map[turns.RunMetadataKey]any): key expression must have type turns.RunMetadataKey (no raw string literals; no untyped string const identifiers)
  • Block.Payload (map[string]any): key must be a const string (no raw literals; no variables)
  • Key constructor locality: turns.DataK/TurnMetaK/BlockMetaK calls are only allowed in generated canonical key files (geppetto/pkg/turns/keys_gen.go, geppetto/pkg/inference/engine/turnkeys_gen.go), app-level *_keys.go, and tests.

Allowed examples (high-level):

  • b.Payload[turns.PayloadKeyText]
  • run.Metadata[turns.RunMetaKeyTraceID]
  • turns.KeyTurnMetaProvider.Get(t.Metadata) (wrapper store access via typed key)
  • engine.KeyToolConfig.Set(&t.Data, engine.ToolConfig{Enabled: true}) (wrapper store access via typed key)

Flagged examples (high-level):

  • b.Payload["text"] (raw string literal)
  • k := turns.PayloadKeyText; b.Payload[k] (variable key)
  • turns.DataK[string]("app", "some_key", 1) in a non-*_keys.go file

Note: b.Payload["text"] can compile because payload keys are strings; the linter exists specifically to force canonical const keys for stability and searchability.

How it works (internals, high level)

turnsdatalint is implemented with golang.org/x/tools/go/analysis. It:

  • Finds AST IndexExpr nodes that look like <something>.Payload[<key>] and enforces const string keys.
  • Finds calls to turns.DataK/TurnMetaK/BlockMetaK and enforces they only appear in key-definition files.
  • Optionally enforces typed key expressions when indexing map fields whose key type matches configured named key types (notably Run.Metadata in Geppetto).

Pseudocode:

for each IndexExpr idx:
  if idx.X is not SelectorExpr ".Data": continue
  if selected field is not map[TurnDataKey]...: continue
  if idx.Index has type TurnDataKey AND is not a raw literal and not an untyped-string const ident: ok
  else: report diagnostic at '['

Configuration

turnsdatalint supports flags for the named key types (fully-qualified):

  • Flags:
    • -turnsdatalint.data-keytype
    • -turnsdatalint.turn-metadata-keytype
    • -turnsdatalint.block-metadata-keytype
    • -turnsdatalint.run-metadata-keytype
  • Defaults: the corresponding github.com/go-go-golems/geppetto/pkg/turns.<...> named types

This is mainly useful if you reuse the analyzer logic for a different struct/map key type in another project.

Running it (in Geppetto and in third-party repos)

If another repo uses Geppetto (for example it imports github.com/go-go-golems/geppetto/pkg/turns), it can reuse the analyzer in two standard ways:

Option 1: Build Geppetto’s bundled lint tool and run it as a vettool

This is the lowest-friction approach when you’re happy to run the analyzers that Geppetto bundles:

go build -o /tmp/geppetto-lint github.com/go-go-golems/geppetto/cmd/geppetto-lint@<version>
go vet -vettool=/tmp/geppetto-lint ./...

Option 2: Embed the analyzer in your own multichecker

If you want to bundle Geppetto’s analyzers plus your own analyzers, create your own lint binary:

  • Main idea:
    • multichecker.Main(turnsdatalint.Analyzer, yourlint.Analyzer, ...)
  • Import:
    • github.com/go-go-golems/geppetto/pkg/analysis/turnsdatalint