Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6e46b20
feat: add -w flag to warn on non-static ExecuteTemplate calls
tooolbox Mar 7, 2026
59342a5
feat: add ParseFiles and ParseGlob support to CLI
tooolbox Mar 7, 2026
8a52f20
feat: replace types.Eval with typesInfo.TypeOf for FuncMap resolution
tooolbox Mar 7, 2026
f6181ea
feat: detect unused templates with -w flag
tooolbox Mar 7, 2026
630d600
feat: warn on field access on interface types with -w flag
tooolbox Mar 7, 2026
7bc9f10
feat: warn on unguarded pointer dereference in templates with -w flag
tooolbox Mar 7, 2026
4d64bc1
test: add nested chain pointer dereference warning test
tooolbox Mar 7, 2026
e182859
feat: per-value nil guard tracking for pointer warnings
tooolbox Mar 7, 2026
d69c5cb
Merge branch 'feature/funcmap-typeof' into next-march
tooolbox Mar 7, 2026
c81896b
Merge branch 'feature/warnings' into next-march
tooolbox Mar 7, 2026
6c3cb42
refactor: introduce WarningCategory for granular warning control
tooolbox Mar 7, 2026
462588e
Merge branch 'feature/unused-templates' into next-march
tooolbox Mar 7, 2026
e194de5
Merge branch 'feature/interface-warnings' into next-march
tooolbox Mar 7, 2026
c6af873
Merge branch 'feature/nil-deref-warnings' into next-march
tooolbox Mar 7, 2026
b4e6adb
docs: add warning and error examples to README
tooolbox Mar 7, 2026
13695b2
fix: change warning output format to position-first style
tooolbox Mar 7, 2026
22bf787
feat: adopt Go-standard diagnostic format with category codes
tooolbox Mar 7, 2026
e89e308
chore: remove UPGRADES.md now that all upgrades are merged
tooolbox Mar 7, 2026
bfaef4b
fix: resolve test failures on Windows from path casing and error format
tooolbox Mar 16, 2026
3407937
feat: resolve non-static ExecuteTemplate names via call-graph tracing
tooolbox Mar 16, 2026
ef6a8c1
feat: add printf validation, Execute support, and unused variable det…
tooolbox Mar 17, 2026
4673363
feat: detect dead conditional branches (W006)
tooolbox Mar 17, 2026
e24c338
feat: warn on sub-templates called with incompatible types (W007)
tooolbox Mar 17, 2026
2cb5422
feat: add gopls analyzer and templatecheck vettool binary
tooolbox Mar 17, 2026
277dbe1
feat: add VS Code extension for in-template diagnostics
tooolbox Mar 17, 2026
380826d
refactor: merge templatecheck vettool into check-templates binary
tooolbox Mar 17, 2026
9622084
test: add real on-disk fixture tests for check-templates
tooolbox Mar 17, 2026
8ad6a4c
revert: remove vettool delegation from check-templates
tooolbox Mar 17, 2026
da8c98a
fix: normalise path separators when matching ParseFS glob patterns on…
tooolbox Mar 17, 2026
6f867c2
ci: add release workflow for Go binaries and VSCode extension
tooolbox Mar 18, 2026
2ed3aca
docs: update README with new warnings, analyzer, and VSCode extension
tooolbox Mar 18, 2026
fa5f208
chore: remove analyzer package
tooolbox Mar 18, 2026
2f922c2
fix: normalise template parse errors to file:line:col format
tooolbox Mar 18, 2026
79c41bf
feat: trace templates through map lookups, closures, and dynamic Pars…
tooolbox Mar 26, 2026
2ff7f37
feat: resolve concrete types through closure parameters passed cross-…
tooolbox Mar 26, 2026
5dcdcb9
feat: trigger template checks on filesystem changes for AI agent support
tooolbox Mar 26, 2026
c8adba9
feat: add --mcp flag to run as MCP server for AI agent integration
tooolbox Mar 26, 2026
3a2abbf
feat: per-page template scoping for map-based template patterns
tooolbox Mar 26, 2026
3eadf73
fix: deduplicate warnings and errors across per-page template sets
tooolbox Mar 26, 2026
3f9feae
fix: suppress false W007 warnings for per-page scoped template sets
tooolbox Mar 26, 2026
2111de1
feat: trace ParseFS fs.FS params across packages to resolve //go:embe…
tooolbox Mar 28, 2026
af73d73
fix: resolve non-literal ParseFS pattern args via SliceEvalContext
tooolbox Mar 28, 2026
7a1fe1a
fix: resolve non-literal ParseFS patterns and fix SliceEvalContext va…
tooolbox Mar 29, 2026
9c728b8
feat: support fs.Glob, path.Base/Dir for per-page scoping with embed.FS
tooolbox Mar 29, 2026
d7d6e47
feat: add templatecheck:"nonil" struct tag to suppress W003 warnings
tooolbox Mar 29, 2026
56c3f87
chore: show --mcp and --version in -h help output
tooolbox Mar 29, 2026
0fadfeb
fix: nonil variable access no longer clobbers {{with}} nil guard
tooolbox Mar 29, 2026
8710ea1
fix: nonil tags and {{if}} guards work through $ variable references
tooolbox Mar 29, 2026
6ad020e
feat: model and short-circuit for nil-safety analysis (W003)
tooolbox Mar 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release

on:
release:
types: [created]

permissions:
contents: write

jobs:
go-binaries:
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'

- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
EXT=""
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
OUTPUT="check-templates-${GOOS}-${GOARCH}${EXT}"
go build -o "$OUTPUT" ./cmd/check-templates

- name: Upload release asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ASSET=$(ls check-templates-*)
gh release upload "${{ github.event.release.tag_name }}" "$ASSET" --clobber

vscode-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Build .vsix
working-directory: vscode-go-template-check
run: |
npm ci
npx @vscode/vsce package --no-git-tag-version

- name: Upload release asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VSIX=$(ls vscode-go-template-check/*.vsix)
gh release upload "${{ github.event.release.tag_name }}" "$VSIX" --clobber
208 changes: 206 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Check [![Go Reference](https://pkg.go.dev/badge/github.com/typelate/check.svg)](https://pkg.go.dev/github.com/typelate/check)

**Check** is a Go library for statically type-checking `text/template` and `html/template`. It catches template/type mismatches early, making refactoring safer when changing types or templates.
**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](#check-templates-cli) and a [VS Code extension](#vs-code-extension).

## `check-templates` CLI

If all your `ExecuteTemplate` calls use a string literal for the template name and a static type for the data argument, you can use the CLI directly:
If all your `ExecuteTemplate` or `Execute` calls use a static type for the data argument, you can use the CLI directly:

```sh
go get -tool github.com/typelate/check/cmd/check-templates
Expand All @@ -13,8 +15,209 @@ go tool check-templates ./...

Flags:
- `-v` — list each call with position, template name, and data type
- `-w` — enable warnings for potential issues (see [Warnings](#warnings) below)
- `-C dir` — change working directory before loading packages
- `-o format` — output format: `tsv` (default) or `jsonl`
- `--mcp` — run as an [MCP](https://modelcontextprotocol.io/) 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.

```go
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:

```go
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.

```go
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
//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.

```go
// 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.

```go
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`) &mdash; not just at Go call sites.

Features:
- Red squiggles on `{{.MissingField}}` in template files
- Yellow squiggles for warnings (W001&ndash;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](./vscode-go-template-check/README.md) for setup and configuration.

## Library usage

Expand All @@ -30,3 +233,4 @@ Call `Execute` with a `types.Type` for the template's data (`.`) and the templat
1. You must provide a `types.Type` for the template's root context (`.`).
2. No support for third-party template packages (e.g. [safehtml](https://pkg.go.dev/github.com/google/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.
Loading