check

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: MIT Imports: 15 Imported by: 0

README

Check Go Reference

Check is a Go library and CLI for statically type-checking text/template and html/template. It catches template/type mismatches early, making refactoring safer when changing types or templates.

It includes a CLI and a VS Code extension.

check-templates CLI

If all your ExecuteTemplate or Execute calls use a static type for the data argument, you can use the CLI directly:

go get -tool github.com/tooolbox/check/cmd/check-templates
go tool check-templates ./...

Flags:

  • -v — list each call with position, template name, and data type
  • -w — enable warnings for potential issues (see Warnings below)
  • -C dir — change working directory before loading packages
  • -o format — output format: tsv (default) or jsonl
  • --mcp — run as an MCP server over stdio (for AI agent integration)
  • --version — print version and exit
How the CLI discovers templates

The CLI works by statically analyzing your Go source code. It traces each ExecuteTemplate and Execute call back to the variable that holds the *template.Template, then follows that variable's initialization chain to find the template files. This means:

  1. ExecuteTemplate must use a string literal for the template name (second argument). Calls that pass a variable or expression will produce a warning (with -w) and be skipped. Execute calls are also supported — the template name is inferred from the receiver's root template.

  2. Template initialization works best with static arguments. File paths passed to ParseFiles, glob patterns passed to ParseGlob, and embed patterns passed to ParseFS are ideally string literals. However, the tool can also trace embed.FS variables through function parameters across packages, resolve fs.Glob calls against embedded file lists, and handle spread []string variables and per-page template map construction.

  3. Supported initialization patterns:

    • template.Must(template.ParseFiles("a.html", "b.html"))
    • template.Must(template.ParseGlob("templates/*.html"))
    • template.Must(template.ParseFS(fs, "*.html"))
    • template.New("name").ParseFiles("a.html")
    • Chained calls: .Funcs(...), .Option(...), .Delims(...), .Parse(...)
    • Additional .ParseFiles(...), .ParseGlob(...), or .ParseFS(...) calls on an already-initialized template variable

Warnings

The -w flag enables warnings for issues that are not type errors but may indicate bugs. All warnings are printed to stderr.

Unguarded pointer dereference

When dot is a pointer type (e.g. *Page), accessing a field like .Title will panic at runtime if dot is nil. The tool warns unless the access is guarded by {{with}}, {{if}}, or the and short-circuit pattern.

type Page struct { Title string }

func render(p *Page) {
    _ = templates.ExecuteTemplate(w, "index.gohtml", p)
}

Warns — accessing .Title on a pointer without a nil guard:

{{.Title}}

OK — guarded with {{with}}:

{{with .}}
  {{.Title}}
{{end}}

OK — guarded with {{if}}:

{{if .}}
  {{.Title}}
{{end}}

OK — guarded with and short-circuit (Go's and returns the first falsy value without evaluating the rest):

{{if and .User (eq .User.Role "admin")}}
  {{.User.Username}}
{{end}}

Guards also work through $ references ($.User), sub-template calls ({{template "nav" .}}), and inside {{range}} blocks.

templatecheck:"nonil" struct tag

For pointer-typed struct fields that are always initialized before being passed to a template, you can suppress W003 with a struct tag:

type PageData struct {
    Title string
    S     *Strings     `templatecheck:"nonil"`
    User  *models.User `templatecheck:"nonil"`
}

The tag is respected for direct access (.S.AppName), variable assignment ($s := .S then $s.AppName), $ references ($.User.Role), and embedded structs.

Interface field access

When dot is interface{} or any, field access cannot be verified at compile time.

func render(data any) {
    _ = templates.ExecuteTemplate(w, "page.gohtml", data)
}

Warns — field access on an interface type:

{{.Title}}
Unused templates

Templates loaded via ParseFS, ParseFiles, or ParseGlob that are never referenced by any ExecuteTemplate call or {{template}} action.

//go:embed *.gohtml
var source embed.FS

var templates = template.Must(template.New("app").ParseFS(source, "*"))

func render() {
    _ = templates.ExecuteTemplate(w, "index.gohtml", data)
}

Warns if unused.gohtml exists in the embed but is never referenced:

main.go:5:5: template "unused.gohtml" is defined but never referenced (W002)
Non-static ExecuteTemplate name

ExecuteTemplate must be called with a string literal for the template name. Calls with a variable or expression cannot be checked statically.

// Warns — template name is a variable, not a string literal:
name := getTemplateName()
_ = templates.ExecuteTemplate(w, name, data)

// OK — template name is a string literal:
_ = templates.ExecuteTemplate(w, "index.gohtml", data)
Unused variables

Variables declared with $x := ... that are never referenced in the template.

{{$x := .Title}}  {{/* $x is never used */}}
<h1>{{.Title}}</h1>
Dead conditional branches

Branches with literal true, false, or nil conditions that can never execute.

{{if true}}always{{else}}never reached (W006){{end}}
{{if false}}never reached (W006){{end}}
Inconsistent sub-template types

A sub-template invoked from multiple {{template}} call sites with incompatible data types.

{{template "header" .Page}}   {{/* passes Page */}}
{{template "header" .Count}}  {{/* passes int — W007 */}}
Warning reference
Code Category
W001 Non-static ExecuteTemplate name
W002 Unused template
W003 Unguarded pointer dereference
W004 Interface field access
W005 Unused variable
W006 Dead conditional branch
W007 Inconsistent sub-template types

Errors

These are type errors that check-templates reports regardless of the -w flag:

Field not found

Accessing a field that does not exist on the data type.

type Page struct { Title string }

Error:

{{.Titel}}  {{/* typo — "Titel" does not exist on Page */}}
Type mismatch in template calls

When {{template "name" .}} passes a type that doesn't match what the sub-template expects.

Printf format mismatch

{{printf "%d" .Name}} where .Name is a string produces a type error. The tool validates that format verbs (%d, %s, %f, etc.) match the types of the corresponding arguments. %v accepts any type.

VS Code extension

The vscode-go-template-check directory contains a VS Code extension that shows diagnostics inside template files (.gohtml, .tmpl, .gotmpl) — not just at Go call sites.

Features:

  • Red squiggles on {{.MissingField}} in template files
  • Yellow squiggles for warnings (W001–W007)
  • Syntax highlighting for Go template directives in .gohtml files
  • Runs automatically on save

The extension requires the check-templates binary on your PATH. See the extension README for setup and configuration.

Library usage

Call Execute with a types.Type for the template's data (.) and the template's parse.Tree. See example_test.go for a working example.

  • muxt — builds on this library to type-check templates wired to HTTP handlers. If you only need command-line checks, muxt check works too.
  • jba/templatecheck — a more mature alternative for template type-checking.

Limitations

  1. You must provide a types.Type for the template's root context (.).
  2. No support for third-party template packages (e.g. safehtml).
  3. Cannot detect runtime conditions such as out-of-range indexes or errors from boxed types.
  4. Template initialization generally requires static arguments, but the tool can trace embed.FS variables through function parameters across packages and resolve fs.Glob patterns against embedded file lists. Dynamically constructed file lists that cannot be statically resolved are skipped gracefully.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Execute

func Execute(global *Global, tree *parse.Tree, data types.Type) error
Example
package main

import (
	"fmt"
	"go/token"
	"log"
	"slices"
	"text/template"
	"text/template/parse"

	"golang.org/x/tools/go/packages"

	"github.com/tooolbox/check"
)

type Person struct {
	Name string
}

func main() {
	// 1. Load Go packages with type info.
	fset := token.NewFileSet()
	pkgs, err := packages.Load(&packages.Config{
		Fset:  fset,
		Tests: true,
		Mode: packages.NeedTypes |
			packages.NeedTypesInfo |
			packages.NeedSyntax |
			packages.NeedFiles |
			packages.NeedName |
			packages.NeedModule,
		Dir: ".",
	}, ".")
	if err != nil {
		log.Fatal(err)
	}
	const testPackageName = "check_test"
	packageIndex := slices.IndexFunc(pkgs, func(p *packages.Package) bool {
		return p.Name == testPackageName
	})
	if packageIndex < 0 {
		log.Fatalf("%s package not found", testPackageName)
	}
	testPackage := pkgs[packageIndex]

	// 2. Parse a template.
	tmpl, err := template.New("example").Parse(
		/* language=gotemplate */ `
{{define "unknown field" -}}
	{{.UnknownField}}
{{- end}}
{{define "known field" -}}
	Hello, {{.Name}}!
{{- end}}"
`)
	if err != nil {
		log.Fatalf("parse error: %v", err)
	}

	// 3. Create a TreeFinder (wraps Template.Lookup).
	treeFinder := check.FindTreeFunc(func(name string) (*parse.Tree, bool) {
		if named := tmpl.Lookup(name); named != nil {
			return named.Tree, true
		}
		return nil, false
	})

	// 4. Build a function checker.
	functions := check.DefaultFunctions(testPackage.Types)

	// 5. Initialize a Global.
	global := check.NewGlobal(testPackage.Types, fset, treeFinder, functions)

	// 6. Look up a type used by the template.
	personObj := testPackage.Types.Scope().Lookup("Person")
	if personObj == nil {
		log.Fatalf("type Person not found in %s", testPackage.PkgPath)
	}

	// 7. Type-check the template.
	{
		const templateName = "unknown field"
		if err := check.Execute(global, tmpl.Lookup("unknown field").Tree, personObj.Type()); err != nil {
			fmt.Println(err.Error())
		} else {
			fmt.Printf("template %q type-check passed\n", templateName)
		}
	}
	{
		const templateName = "known field"
		if err := check.Execute(global, tmpl.Lookup("known field").Tree, personObj.Type()); err != nil {
			fmt.Println(err.Error())
		} else {
			fmt.Printf("template %q type-check passed\n", templateName)
		}
	}
}
Output:
example:3:3: executing "unknown field" at <.UnknownField>: UnknownField not found on github.com/tooolbox/check_test.Person (E001)
template "known field" type-check passed

func Package

Package discovers all .ExecuteTemplate calls in the given package, resolves receiver variables to their template construction chains, and type-checks each call.

ExecuteTemplate must be called with a string literal for the second parameter. If warn is non-nil, it is called for non-fatal issues such as unused templates or unguarded pointer access.

Types

type CallChecker

type CallChecker interface {
	CheckCall(*Global, string, []parse.Node, []types.Type) (types.Type, error)
}

type DeferredCall

type DeferredCall struct {

	// FuncObj is the exported function that wraps the ExecuteTemplate call.
	FuncObj types.Object
	// NameParamIdx is the parameter index providing the template name (-1 if resolved).
	NameParamIdx int
	// DataParamIdx is the parameter index providing the data (-1 if resolved).
	DataParamIdx int
	// ReceiverParamIdx is the parameter index providing the template receiver (-1 if resolved).
	ReceiverParamIdx int
	// contains filtered or unexported fields
}

DeferredCall represents an ExecuteTemplate call whose template name or data type could not be resolved within its own package. Callers from other packages may provide the concrete values via call-graph tracing.

func PackageWithDeferred

func PackageWithDeferred(pkg *packages.Package, inspectCall ExecuteTemplateNodeInspectorFunc, inspectTemplate TemplateNodeInspectorFunc, warn PackageWarningFunc, imported []DeferredCall, allPkgs []*packages.Package) ([]DeferredCall, error)

PackageWithDeferred is like Package but also accepts deferred calls from dependency packages and returns any new deferred calls discovered in this package. This enables cross-package call-graph tracing. allPkgs, if provided, enables cross-package parameter resolution for template construction tracing.

type Error

type Error struct {
	Tree *parse.Tree
	Node parse.Node
	// contains filtered or unexported fields
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type ExecuteTemplateNodeInspectorFunc

type ExecuteTemplateNodeInspectorFunc func(node *ast.CallExpr, t *parse.Tree, tp types.Type)

type FindTreeFunc

type FindTreeFunc func(name string) (*parse.Tree, bool)

func (FindTreeFunc) FindTree

func (fn FindTreeFunc) FindTree(name string) (*parse.Tree, bool)

type Functions

type Functions map[string]*types.Signature

func DefaultFunctions

func DefaultFunctions(pkg *types.Package) Functions

DefaultFunctions returns the standard functions defined in html/template and text/template. It looks up escape functions (js, html, urlquery) from whichever template package is imported.

func (Functions) Add

func (functions Functions) Add(m Functions) Functions

func (Functions) CheckCall

func (functions Functions) CheckCall(global *Global, funcIdent string, argNodes []parse.Node, argTypes []types.Type) (types.Type, error)

type Global

type Global struct {
	InspectTemplateNode TemplateNodeInspectorFunc
	InspectCallNode     ExecuteTemplateNodeInspectorFunc
	Warn                WarningFunc

	// Qualifier controls how types are printed in error messages.
	// If nil, types are printed with their full package path.
	// See types.WriteType for details.
	Qualifier types.Qualifier
	// contains filtered or unexported fields
}

func NewGlobal

func NewGlobal(pkg *types.Package, fileSet *token.FileSet, trees TreeFinder, fnChecker CallChecker) *Global

func (*Global) TypeString

func (g *Global) TypeString(typ types.Type) string

TypeString returns the string representation of typ using the configured Qualifier.

type PackageWarningFunc

type PackageWarningFunc func(category WarningCategory, pos token.Position, message string)

PackageWarningFunc is called when a non-fatal issue is detected. The category identifies the warning type, allowing callers to filter.

type TemplateNodeInspectorFunc

type TemplateNodeInspectorFunc func(node *parse.TemplateNode, t *parse.Tree, tp types.Type)

type TreeFinder

type TreeFinder interface {
	FindTree(name string) (*parse.Tree, bool)
}

TreeFinder should wrap https://pkg.go.dev/html/template#Template.Lookup and return the Tree field from the Template If you are using text/template the lookup function from that package should also work.

type TypeNodeMapping

type TypeNodeMapping map[types.Type][]parse.Node

type WarningCategory

type WarningCategory int

WarningCategory identifies the kind of warning.

const (
	// WarnNonStaticTemplateName indicates an ExecuteTemplate call with a
	// non-static string for the template name.
	WarnNonStaticTemplateName WarningCategory = iota + 1

	// WarnUnusedTemplate indicates a template that is defined but never
	// referenced by any ExecuteTemplate call or {{template}} action.
	WarnUnusedTemplate

	// WarnNilDereference indicates field access on a pointer type without
	// a nil guard ({{with}} or {{if}}).
	WarnNilDereference

	// WarnInterfaceFieldAccess indicates field access on an interface type
	// that cannot be statically verified.
	WarnInterfaceFieldAccess

	// WarnUnusedVariable indicates a template variable that is declared
	// (via $x := ...) but never referenced.
	WarnUnusedVariable

	// WarnDeadBranch indicates a conditional branch that can never execute
	// because the condition is a literal true, false, or nil constant.
	WarnDeadBranch

	// WarnInconsistentTemplateTypes indicates that a sub-template is
	// invoked from multiple {{template}} call sites with incompatible
	// data types, which will produce different runtime behaviour depending
	// on the caller.
	WarnInconsistentTemplateTypes
)

func (WarningCategory) Code

func (c WarningCategory) Code() string

Code returns the short diagnostic code for the warning category (e.g. "W001").

type WarningFunc

type WarningFunc func(category WarningCategory, tree *parse.Tree, node parse.Node, message string)

WarningFunc is called when a non-fatal issue is detected during type-checking, such as field access on an interface type or unguarded pointer dereference.

Directories

Path Synopsis
cmd
check-templates command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL