Draft
Conversation
Add WarningFunc to the check.Package API and a -w CLI flag that emits warnings when ExecuteTemplate is called with a non-static template name. Warnings are off by default to avoid noise in existing workflows. Also add tests for ./... with deeply nested packages and document how the CLI discovers templates (embed.FS requirement, supported patterns). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CLI can now analyze templates loaded via template.ParseFiles() and
template.ParseGlob() with string literal arguments, in addition to the
existing embed.FS + ParseFS support.
Supported in all contexts: package-level calls (template.ParseFiles),
variable receiver calls (ts.ParseFiles), and chained calls
(template.New("x").ParseFiles).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
types.Eval required formatting AST back to source text and re-evaluating, which was fragile for method values and package-level variables. Using typesInfo.TypeOf directly resolves function signatures from the already- computed type information, handling all expression forms correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add TemplateNames() to the Template interface to enumerate all defined
templates. After type-checking, compare referenced templates (from
ExecuteTemplate calls and {{template}} actions) against all available
templates and warn about unreferenced ones.
Templates with no content (e.g. the root container from template.New)
are excluded from the check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When dot or a field is an interface type (e.g. any), field access like .Title cannot be statically verified. Instead of producing a hard error, emit a warning when -w is enabled and continue type-checking with the field treated as an empty interface. Adds WarningFunc to Global for template-level warnings, PackageWarningFunc for package-level warnings, and bridges them through checkCalls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a template accesses fields on a pointer type (e.g. *Page) without
a {{with}} or {{if}} guard, the access may panic at runtime if the
pointer is nil. With -w enabled, emit a warning suggesting a nil guard.
Adds dotGuarded tracking to scope: set to true inside {{with}} and
{{if}} blocks so that guarded pointer access does not trigger warnings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verifies that .Inner.Value warns when Inner is *Inner, catching pointer dereference at any depth in a field chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the coarse dotGuarded bool with a per-field-path guarded set.
{{with .Foo}} guards only .Foo (and dot inside the block), not unrelated
fields like .Bar. {{if .Bar}} guards .Bar within its block.
Guarding .Foo does NOT guard .Foo.Baz — if Baz is a pointer type,
accessing fields through it still warns unless .Foo.Baz is separately
guarded.
Adds pipeFieldPath helper to extract the tested field path from
{{with}}/{{if}} pipe expressions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # README.md
Add WarningCategory enum to PackageWarningFunc callback so library consumers can filter warnings by type. CLI continues to use single -w flag that enables all categories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document all four warning categories with Go code and template examples showing what triggers each warning and how to fix it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change from "warning: pos: message" to "pos: warning - message" to be consistent with Go tooling diagnostic conventions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warnings: file:line:col: message (W00N) Errors: file:line:col: executing "name" at <.Field>: message (E001) Remove the "type check failed:" prefix from errors and "warning -" prefix from warnings to match go vet / staticcheck conventions. Add diagnostic codes (W001-W004, E001) for machine-parseable output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use case-insensitive filepath comparison in check_test.go and stdlib_test.go to handle Windows drive letter casing differences between runtime.Caller and packages.Load. Also update convertTextExecError to match the new diagnostic format with category codes introduced in 22bf787. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ResolveStringExpr to statically resolve template names from named constants, simple variable assignments, and function parameters by tracing through the intra-package and cross-package call graph. Tier 1: Named constants (const name = "page") Tier 2: Simple variables (name := "page"), with reassignment detection Tier 3: Function parameter tracing via call-site index Tier 4: Interface data type resolution (any → concrete type at call site) Tier 5: Multi-level call chains with fixed-point iteration (up to 5 levels) Tier 6: Cross-package tracing via PackageWithDeferred/DeferredCall API This replaces the single BasicLiteralString check in findExecuteCalls with a progressive resolution pipeline that handles the most common patterns for wrapping ExecuteTemplate in helper functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ection - checkPrintf in func.go validates format verb count and types (W-codes emitted via existing error path; arg-count mismatch and type mismatch both reported as hard errors consistent with other type checks) - package.go now recognises tpl.Execute(w, data) 2-arg calls and resolves the template name from the receiver's root template - check.go tracks variable declarations vs uses per scope and emits W005 (WarnUnusedVariable) for $x declared but never read - Script tests added for all three features (pass/err/warn variants) - PLAN.md added documenting the full 7-feature roadmap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add WarnDeadBranch warning emitted when an {{if}} or {{with}} block
uses a literal true or false BoolNode as its condition, making one
branch statically unreachable. The nil literal case is excluded because
the template parser itself rejects {{if nil}} before analysis runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track every (templateName, dataType) pair seen at {{template "name" .Data}}
call sites during Execute. After the walk, emit WarnInconsistentTemplateTypes
(W007) if the same sub-template is invoked with mutually non-assignable
types across different call sites. Untyped nil and empty interface are
excluded from the check as they carry no structural information.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
analyzer/analyzer.go wraps check.Package as a go/analysis.Analyzer. It constructs a packages.Package directly from the analysis.Pass fields (reusing the already type-checked AST) and expands //go:embed directives from AST comments to populate EmbedFiles without a second packages.Load. Diagnostics are reported at the ExecuteTemplate call site in Go source so gopls shows squiggles in .go files. The -w flag enables warnings. cmd/templatecheck/main.go is a singlechecker binary so the analyzer can be used as: go vet -vettool=$(which templatecheck) ./... Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vscode-go-template-check/ is a VS Code extension that spawns check-templates on save and places error/warning squiggles inside .gohtml template files at the exact positions the tool reports. Key design points: - No LSP: lightweight CLI-spawn on save, stderr parsed into DiagnosticCollection entries per template file URI - New language ID `gohtml` for .gohtml files only — no conflict with vscode-go's `gotmpl` registration - TextMate grammar (text.html.gotemplate) embeds text.html.basic and injects Go template keyword/variable/field highlighting - Binary distribution follows gopls pattern: check on activation, prompt user to `go install` if not found on PATH - Build requires only `go install github.com/evanw/esbuild/cmd/esbuild@latest`; npm is only needed for `npx @vscode/vsce package` - esbuild listed as devDependency so vscode:prepublish works via vsce Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detect go vet vettool probe flags (-V, -flags, -json) and delegate to singlechecker.Main, eliminating the separate cmd/templatecheck binary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TestFixtures discovers subdirs under testdata/fixtures/, reads fixture.json for flags/expectations, and runs check-templates against actual Go and .gotmpl files. Covers pass, error, and warning cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The go/analysis path only sees what go vet provides — no EmbedFiles, no cross-package deferred resolution — so it gives incomplete coverage and a false sense of security. The standalone binary is the right entry point. The analyzer package is kept for gopls/editor integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… Windows
filepath.Match("templates/*.gotmpl", "templates\page.gotmpl") returns false
on Windows because the embedded file path uses backslashes while the pattern
from the Go source literal uses forward slashes. Apply filepath.FromSlash to
the pattern before matching, consistent with embeddedFilesMatchingTemplateNameList.
Verified with new fixtures: cross_pkg_subdir_pass and cross_pkg_subdir_err,
both using templates in a pkg/templates/ subdirectory called from pkg/exec.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Builds check-templates for linux/darwin/windows (amd64+arm64) and packages the .vsix, uploading both as GitHub release assets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add documentation for W005-W007 warnings, printf format validation, Execute support, gopls analyzer, and VS Code extension. Remove PLAN.md now that all planned features are implemented. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The go/analysis framework doesn't provide EmbedFiles or cross-package deferred resolution, so the analyzer gives incomplete coverage compared to the standalone CLI. The VS Code extension already calls the CLI directly, making this package unused dead code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go's template parser produces errors like "template: name:line: message" which the VS Code extension regex couldn't match. Rewrite them to "filepath:line:1: message (E001)" so parse errors show up as squiggles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eFiles Adds deep template tracing for the common Go pattern where templates are loaded into a map by a helper function (e.g. loadTemplates), then retrieved via map lookup in a closure. This enables type-checking for projects like "beyond" that use filepath.Glob + ParseFiles(files...) with cross-package parameter resolution. Key changes: - Map-index tracing (traceMapIndex, findMapStore, findMapStoreInFunc) - String-slice expression evaluator (sliceval.go) for filepath.Join/Glob/Base - Closure parameter support in IsFuncParam (FuncLit + variable assignment) - Cross-package parameter resolution (buildDeepParamBindings, resolveStringDeep) - FindDefiningValue exported; FindDefiningValueInBlock added for block-scoped lookup - EvaluateTemplateSelector/evaluateParseFilesArgs accept SliceEvalContext for spread and non-literal ParseFiles args - PackageWithDeferred accepts allPkgs for cross-package param resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…package When a closure (e.g. render) is passed as an argument to a function in another package, trace into that function's body to find calls to the parameter with concrete types. This resolves the data type for template type-checking instead of leaving it as `any`. Also deduplicates pending calls to prevent repeated errors when the same template is reached through multiple indirect call sites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add FileSystemWatcher for .go and template files so the extension runs check-templates when files are written directly to disk (not just on manual save). Debounced at 1.5s to batch rapid writes. Also adds .html to watched template extensions. Bumps extension version to 0.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a --mcp flag that starts check-templates as a Model Context Protocol server over stdio, exposing a check_templates tool that AI agents (Claude Code, etc.) can call to type-check Go templates. Uses mcp-go v0.46.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When templates are stored in a map per page (the common loadTemplates +
for-range + ParseFiles pattern), each render call is now checked against
only the template set for that specific page. This eliminates cross-page
type contamination where one page's {{define "content"}} fields were
falsely checked against another page's data type.
Key changes:
- pendingCall tracks mapKeyParamIdx/mapKey for map index key resolution
- findMapStoreInFunc returns loop context (range var, range expr, key expr)
- resolveTemplates builds per-key template sets via WithBinding
- checkCalls selects the correct per-page set based on mapKey
- Exported FindEnclosingFuncLit, FuncLitVarObj, ResolveString in asteval
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shared templates (e.g. nav.html, base.html) are included in every per-page template set, causing the same warning to be emitted once per page. Deduplicates by (position, message) for warnings and by error string for errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-page scoped calls legitimately pass different types to shared
sub-templates like {{template "content" .}}. Exclude them from the
cross-call sub-template type accumulation that powers W007, since each
page is already individually type-checked by Execute.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d vars When ParseFS is called with an fs.FS that is a function parameter rather than a package-level var, the tool now traces back through the call graph across all packages to find the originating //go:embed var declaration. This eliminates false-positive "variable not found" errors for common patterns like passing embed.FS through constructor functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When ParseFS is called with a spread []string variable instead of string literals (e.g. ParseFS(fsys, files...)), fall back to resolving the patterns via SliceEvalContext rather than erroring with "expected string literal". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…riable shadowing - Thread SliceEvalContext into evaluateCallParseFilesArgs so ParseFS with spread []string args (e.g. ParseFS(fsys, files...)) can be resolved - Fix variable shadowing bug in resolveIdentSlice where the ok from a type assertion prevented FindDefiningValue from being called when ctx.Block was nil - Gracefully skip pattern filtering when patterns can't be resolved rather than reporting false-positive errors - Ensure a default SliceEvalContext is created in resolveTemplates when traceMapIndex doesn't produce one - Add tests for spread pattern resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add fs.Glob resolution in SliceEvalContext by tracing the fs.FS argument back through the EmbedFSResolver to get the correct embedded file paths. Add path.Base and path.Dir support (forward-slash path package) alongside the existing filepath equivalents. Return forward-slash paths from resolveFSGlob to match embed.FS runtime behavior. Skip absolute-path conversion for forward-slash embed paths in the per-page scoping loop. This enables full per-page template scoping for the common pattern of passing embed.FS through constructor functions and using fs.Glob + path.Base/Dir to build per-page template sets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pointer-typed struct fields tagged with `templatecheck:"nonil"` are treated as known non-nil, suppressing W003 nil-dereference warnings. The tag propagates through template variable assignment ($s := .S) and works with embedded structs. Updated W003 message to mention the tag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The defer delete(s.guarded, ".") in checkVariableNode for nonil-tagged
variables was unconditionally removing the "." guard entry, which
clobbered the {{with}} block's nil-safety guard when a nonil variable
was accessed inside a {{with}} block. Fixed to only remove the guard
if it was not already present. Also propagate the "." guard from parent
scope to child template scopes in checkTemplateNode so sub-templates
called from within {{with}} blocks inherit the nil safety.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix two issues with W003 nil-dereference suppression via $:
1. The nonil field cache was only storing the first queried field per
struct, causing subsequent lookups for different tagged fields on the
same struct to miss. Now scans and caches all tagged fields on first
access.
2. {{if $.Field}} guards produced $-prefixed paths (e.g. "$User") but
checkIdentifiers built .-prefixed paths (e.g. ".User"), so guards
were never matched for $ references. Now translates $-prefixed
guarded paths to .-prefixed form when checking $ variable access.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the built-in "and" function is used, evaluate arguments left-to-right
and propagate nil-safety from earlier arguments to later ones. If argument
N is a bare variable or field reference to a pointer type, arguments N+1
through last are evaluated with that path guarded. The guard also applies
to the body of {{if}} and {{with}} blocks containing "and" chains.
This matches Go template runtime semantics where "and" returns the first
falsy value without evaluating the rest.
Also updates README to document templatecheck:"nonil" struct tag, "and"
short-circuit support, --mcp flag, and updated limitations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch adds five new static analysis checks, a VS Code extension for in-template diagnostics, and CI for automated releases.
New checks
{{printf "%d" .Name}}where.Nameis a string is now a type error. Format verbs are validated against argument types;%vis always accepted.template.Executesupport — 2-argExecutecalls are now discovered and checked, not justExecuteTemplate.{{$x := .Foo}}where$xis never referenced.{{if true}}...{{else}}unreachable{{end}}and similar literal-constant conditions.{{template "name"}}is called with incompatible data types from different call sites.Improved existing checks
ExecuteTemplatename resolution — call-graph tracing now resolves template names passed through function parameters, covering helper/wrapper patterns.ParseFilesandParseGlobsupport — template initialization viaParseFiles(...)andParseGlob(...)is now traced alongside the existingParseFSsupport.types.Evalapproach withtypesInfo.TypeOffor resolving FuncMap entries.VS Code extension
New
vscode-go-template-check/extension shows diagnostics inside template files (.gohtml,.tmpl,.gotmpl):{{.MissingField}}check-templatesif not foundCI / Release automation
.github/workflows/release.yml— on GitHub release creation, buildscheck-templatesbinaries for linux/darwin/windows (amd64 + arm64) and packages the.vsix, uploading all as release assets.What we didn't ship
go/analysisanalyzer / vettool integration — We built ago/analysis.Analyzerand wired it into the CLI as ago vet -vettool=backend. We reverted and then removed it entirely. Thego/analysisframework doesn't provide access toEmbedFilesor cross-package deferred resolution, so it gives incomplete coverage compared to the standalone CLI — a false sense of security. The VS Code extension calls the CLI directly, which is the correct entry point.Testing
Warning reference
ExecuteTemplatenameTest plan
go test ./...passesgo tool check-templates ./...runs cleanly on a sample project.gohtmlfiles